From 48427e92a872ac63c0c4da443357475dd6a6d43d Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 01:48:57 +0900 Subject: [PATCH 01/21] =?UTF-8?q?[#45]=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B2=B0=EA=B3=BC=EC=B0=BD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Data/DataSources/SearchDataSource.swift | 56 ++++++- .../Repositories/SearchRepositoryImpl.swift | 4 + .../Domain/Protocols/SearchRepository.swift | 1 + .../Domain/UseCases/SearchUseCase.swift | 4 + .../Presentation/View/SearchResultView.swift | 137 ++++++++++++++---- .../ViewModel/SearchResultViewModel.swift | 17 +++ .../DesignSystem/Views/CustomUserRow.swift | 75 +++++++--- 7 files changed, 247 insertions(+), 47 deletions(-) diff --git a/Codive/Features/Search/Data/DataSources/SearchDataSource.swift b/Codive/Features/Search/Data/DataSources/SearchDataSource.swift index 5b21c32d..cc7c8cdc 100644 --- a/Codive/Features/Search/Data/DataSources/SearchDataSource.swift +++ b/Codive/Features/Search/Data/DataSources/SearchDataSource.swift @@ -9,7 +9,7 @@ import Foundation final class SearchDataSource { - // MARK: - Fetch Methods + // MARK: - Fetch Methods (기존) func fetchUserName() -> SearchEntity { return SearchEntity(username: "코디브") @@ -55,7 +55,26 @@ final class SearchDataSource { } } - // MARK: - Private Methods + // MARK: - Fetch Methods (계정용 추가) + + /// 검색어를 기준으로 유저 목록을 필터링해서 반환 + func fetchUsers(query: String) -> [SimpleUser] { + let allUsers = getAllUsers() + + // 전체 or 빈 문자열이면 전부 리턴 + if query.isEmpty || query == "전체" { + return allUsers + } + + let lowercasedQuery = query.lowercased() + + return allUsers.filter { user in + user.nickname.lowercased().contains(lowercasedQuery) + || user.handle.lowercased().contains(lowercasedQuery) + } + } + + // MARK: - Private Methods (공통) private func createDate(year: Int, month: Int, day: Int) -> Date { var components = DateComponents() @@ -65,6 +84,8 @@ final class SearchDataSource { return Calendar.current.date(from: components) ?? Date() } + // MARK: - Private Methods (게시글 더미) + private func getAllPosts() -> [PostEntity] { return [ PostEntity( @@ -105,4 +126,35 @@ final class SearchDataSource { ) ] } + + // MARK: - Private Methods (유저 더미) + + private func getAllUsers() -> [SimpleUser] { + return [ + SimpleUser( + userId: 1, + nickname: "코디브 공식", + handle: "@codive_official", + avatarURL: URL(string: "https://picsum.photos/id/200/80/80") + ), + SimpleUser( + userId: 2, + nickname: "한금준", + handle: "@geumjoon", + avatarURL: URL(string: "https://picsum.photos/id/201/80/80") + ), + SimpleUser( + userId: 3, + nickname: "드뮤어룩 장인", + handle: "@demure_master", + avatarURL: URL(string: "https://picsum.photos/id/202/80/80") + ), + SimpleUser( + userId: 4, + nickname: "한강러버", + handle: "@hanriver_lover", + avatarURL: nil + ) + ] + } } diff --git a/Codive/Features/Search/Data/Repositories/SearchRepositoryImpl.swift b/Codive/Features/Search/Data/Repositories/SearchRepositoryImpl.swift index cd866f82..4e324d4e 100644 --- a/Codive/Features/Search/Data/Repositories/SearchRepositoryImpl.swift +++ b/Codive/Features/Search/Data/Repositories/SearchRepositoryImpl.swift @@ -31,4 +31,8 @@ final class SearchRepositoryImpl: SearchRepository { func fetchPosts(query: String) -> [PostEntity] { return datasource.fetchPosts(query: query) } + + func fetchUsers(query: String) -> [SimpleUser] { + return datasource.fetchUsers(query: query) + } } diff --git a/Codive/Features/Search/Domain/Protocols/SearchRepository.swift b/Codive/Features/Search/Domain/Protocols/SearchRepository.swift index 1fa80d83..7b8fc9dd 100644 --- a/Codive/Features/Search/Domain/Protocols/SearchRepository.swift +++ b/Codive/Features/Search/Domain/Protocols/SearchRepository.swift @@ -10,4 +10,5 @@ protocol SearchRepository { func fetchRecentSearchTags() -> [SearchTagEntity] func fetchRecommendedNews() -> [NewsEntity] func fetchPosts(query: String) -> [PostEntity] + func fetchUsers(query: String) -> [SimpleUser] } diff --git a/Codive/Features/Search/Domain/UseCases/SearchUseCase.swift b/Codive/Features/Search/Domain/UseCases/SearchUseCase.swift index 497e3256..0290d4ef 100644 --- a/Codive/Features/Search/Domain/UseCases/SearchUseCase.swift +++ b/Codive/Features/Search/Domain/UseCases/SearchUseCase.swift @@ -31,4 +31,8 @@ final class SearchUseCase { func fetchPosts(query: String) -> [PostEntity] { return repository.fetchPosts(query: query) } + + func fetchUsers(query: String) -> [SimpleUser] { + return repository.fetchUsers(query: query) + } } diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index 7bfcf547..ee5f4dd7 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -7,9 +7,15 @@ import SwiftUI +enum SearchResultSegment { + case account + case hashtag +} + struct SearchResultView: View { // MARK: - Properties @StateObject private var viewModel: SearchResultViewModel + @State private var selectedSegment: SearchResultSegment = .account // MARK: - Computed Properties private var sortOptionsString: [String] { @@ -35,37 +41,25 @@ struct SearchResultView: View { viewModel.executeNewSearch(query: viewModel.searchBarText) } + SearchResultSegmentControl(selectedSegment: $selectedSegment) + .padding(.top, 16) + ScrollView { - VStack { - HStack { - Text("\(TextLiteral.Search.totalCount) \(viewModel.posts.count)\(TextLiteral.Search.countUnit)") - .font(Font.codive_body2_medium) - .foregroundStyle(Color.Codive.grayscale3) - Spacer() - - SortOption( - mainText: TextLiteral.Search.sortAll, - options: sortOptionsString, - selectedOption: $viewModel.currentSort - ) - .zIndex(10) - } - .padding(.top, 18) - .zIndex(10) - - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 11) { - ForEach(viewModel.posts) { post in - PostCard( - postImageUrl: post.postImageUrl, - profileImageUrl: post.profileImageUrl, - nickname: post.nickname - ) + if selectedSegment == .account { + VStack(spacing: 0) { + ForEach(viewModel.users, id: \.userId) { user in + CustomUserRow( + user: user, + buttonStyle: .none + ) { + // 버튼 동작 + } } } .padding(.top, 18) - .zIndex(1) + } else { + Hashtag(viewModel: viewModel) } - .padding(.bottom, 20) } } .navigationBarHidden(true) @@ -73,7 +67,96 @@ struct SearchResultView: View { .padding(.horizontal, 20) // MARK: - Data Loading Trigger .onAppear { - viewModel.loadPosts() + viewModel.loadInitialData() + } + } +} + +// 아래 Hashtag / SearchResultSegmentControl는 너가 만든 버전 그대로 사용 + +struct Hashtag: View { + @ObservedObject var viewModel: SearchResultViewModel + + // MARK: - Computed Properties + private var sortOptionsString: [String] { + viewModel.sortOptions.map { $0.displayName } + } + + // MARK: - Body + var body: some View { + VStack { + HStack { + Text("\(TextLiteral.Search.totalCount) \(viewModel.posts.count)\(TextLiteral.Search.countUnit)") + .font(Font.codive_body2_medium) + .foregroundStyle(Color.Codive.grayscale3) + Spacer() + + SortOption( + mainText: TextLiteral.Search.sortAll, + options: sortOptionsString, + selectedOption: $viewModel.currentSort + ) + .zIndex(10) + } + .padding(.top, 18) + .zIndex(10) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 11) { + ForEach(viewModel.posts) { post in + PostCard( + postImageUrl: post.postImageUrl, + profileImageUrl: post.profileImageUrl, + nickname: post.nickname + ) + } + } + .padding(.top, 18) + .zIndex(1) + } + .padding(.bottom, 20) + } +} + +struct SearchResultSegmentControl: View { + @Binding var selectedSegment: SearchResultSegment + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + segmentItem(title: "계정", segment: .account) + segmentItem(title: "해시태그", segment: .hashtag) + } + + Rectangle() + .frame(height: 1) + .foregroundStyle(Color.Codive.grayscale4) + } + } + + @ViewBuilder + private func segmentItem(title: String, segment: SearchResultSegment) -> some View { + Button { + selectedSegment = segment + } label: { + VStack(spacing: 6) { + Text(title) + .font(.codive_body1_medium) + .foregroundStyle( + selectedSegment == segment + ? Color.Codive.grayscale1 + : Color.Codive.grayscale3 + ) + + Rectangle() + .frame(height: 2) + .foregroundStyle( + selectedSegment == segment + ? Color.Codive.main0 + : .clear + ) + } + .frame(maxWidth: .infinity) } + .buttonStyle(.plain) } } diff --git a/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift b/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift index 748ba127..c82a1a2d 100644 --- a/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift +++ b/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift @@ -14,9 +14,11 @@ final class SearchResultViewModel: ObservableObject { private let navigationRouter: NavigationRouter private let useCase: SearchUseCase private var allPosts: [PostEntity] = [] + private var allUsers: [SimpleUser] = [] private var initialQuery: String @Published var posts: [PostEntity] = [] + @Published var users: [SimpleUser] = [] // 🔸 계정 탭용 @Published var currentSort: String = "전체" @Published var searchBarText: String @@ -65,12 +67,24 @@ final class SearchResultViewModel: ObservableObject { // MARK: - Public Methods + /// 게시글 + 유저를 한 번에 초기 로딩 + func loadInitialData() { + loadPosts() + loadUsers() + } + func loadPosts() { self.allPosts = useCase.fetchPosts(query: self.initialQuery) self.posts = self.allPosts self.applySorting(newSort: self.currentSort) } + func loadUsers() { + self.allUsers = useCase.fetchUsers(query: self.initialQuery) + self.users = self.allUsers + print("유저 로딩 완료: \(self.users.count)명") + } + func executeNewSearch(query: String) { let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedQuery.isEmpty { @@ -80,7 +94,10 @@ final class SearchResultViewModel: ObservableObject { self.initialQuery = trimmedQuery self.currentSort = "전체" + + // 🔸 새 검색 시 게시글 + 유저 둘 다 갱신 loadPosts() + loadUsers() navigationRouter.navigate(to: .searchResult(query: trimmedQuery)) print("새로운 검색 실행: \(trimmedQuery)") diff --git a/Codive/Shared/DesignSystem/Views/CustomUserRow.swift b/Codive/Shared/DesignSystem/Views/CustomUserRow.swift index 8fc402c3..3c178a06 100644 --- a/Codive/Shared/DesignSystem/Views/CustomUserRow.swift +++ b/Codive/Shared/DesignSystem/Views/CustomUserRow.swift @@ -10,23 +10,59 @@ import SwiftUI enum CustomUserRowButtonStyle { case primary // 채워진 스타일 (e.g. 팔로우) case secondary // 테두리 스타일 (e.g. 팔로잉, 차단 해제) + case none // 버튼이 없는 상태 } struct CustomUserRow: View { let user: SimpleUser - let buttonTitle: String + let buttonTitle: String? let buttonStyle: CustomUserRowButtonStyle let action: () -> Void + + // MARK: - Convenience Initializers + + /// 버튼이 있는 경우 (팔로우 / 팔로잉 / 차단 해제 등) + init( + user: SimpleUser, + buttonTitle: String, + buttonStyle: CustomUserRowButtonStyle, + action: @escaping () -> Void + ) { + self.user = user + self.buttonTitle = buttonTitle + self.buttonStyle = buttonStyle + self.action = action + } + + /// 버튼이 없는 경우 + init( + user: SimpleUser, + buttonStyle: CustomUserRowButtonStyle = .none, + action: @escaping () -> Void + ) { + self.user = user + self.buttonTitle = nil + self.buttonStyle = buttonStyle + self.action = action + } + // MARK: - Body + var body: some View { HStack { // 아바타 AsyncImage(url: user.avatarURL) { phase in switch phase { - case .success(let img): img.resizable().scaledToFill() - case .empty: Color.Codive.grayscale4 - case .failure: Color.Codive.grayscale4 - @unknown default: Color.Codive.grayscale4 + case .success(let img): + img + .resizable() + .scaledToFill() + case .empty: + Color.Codive.grayscale4 + case .failure: + Color.Codive.grayscale4 + @unknown default: + Color.Codive.grayscale4 } } .frame(width: 40, height: 40) @@ -45,20 +81,22 @@ struct CustomUserRow: View { Spacer() - Button(action: action) { - Text(buttonTitle) - .font(.codive_body2_medium) - .foregroundStyle(buttonStyle == .primary ? .white : Color.Codive.main0) - .frame(minWidth: 76, minHeight: 32) - .background(buttonStyle == .primary ? Color.Codive.main0 : .white) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(buttonStyle == .secondary ? Color.Codive.main0 : .clear, lineWidth: 1) - ) - .multilineTextAlignment(.center) + if buttonStyle != .none { + Button(action: action) { + Text(buttonTitle ?? "") + .font(.codive_body2_medium) + .foregroundStyle(buttonStyle == .primary ? .white : Color.Codive.main0) + .frame(minWidth: 76, minHeight: 32) + .background(buttonStyle == .primary ? Color.Codive.main0 : .white) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(buttonStyle == .secondary ? Color.Codive.main0 : .clear, lineWidth: 1) + ) + .multilineTextAlignment(.center) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) } .padding(.horizontal, 20) } @@ -76,5 +114,6 @@ struct CustomUserRow: View { CustomUserRow(user: dummyUser, buttonTitle: "팔로우", buttonStyle: .primary) { } CustomUserRow(user: dummyUser, buttonTitle: "팔로잉", buttonStyle: .secondary) { } CustomUserRow(user: dummyUser, buttonTitle: "차단 해제", buttonStyle: .secondary) { } + CustomUserRow(user: dummyUser, buttonStyle: .none) { } } } From 0c7596793e2a28617707e7e136dc58f42e1f73b7 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 01:50:41 +0900 Subject: [PATCH 02/21] =?UTF-8?q?[#45]=20Mark=20=EC=A3=BC=EC=84=9D=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 #45 --- .../Search/Presentation/View/SearchResultView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index ee5f4dd7..ca2547e8 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -7,11 +7,13 @@ import SwiftUI +// MARK: - Segment Type enum SearchResultSegment { case account case hashtag } +// MARK: - Search Result struct SearchResultView: View { // MARK: - Properties @StateObject private var viewModel: SearchResultViewModel @@ -45,6 +47,7 @@ struct SearchResultView: View { .padding(.top, 16) ScrollView { + // MARK: - Account Result List if selectedSegment == .account { VStack(spacing: 0) { ForEach(viewModel.users, id: \.userId) { user in @@ -58,6 +61,7 @@ struct SearchResultView: View { } .padding(.top, 18) } else { + // MARK: - Hashtag Result Grid Hashtag(viewModel: viewModel) } } @@ -72,8 +76,7 @@ struct SearchResultView: View { } } -// 아래 Hashtag / SearchResultSegmentControl는 너가 만든 버전 그대로 사용 - +// MARK: - Hashtag View struct Hashtag: View { @ObservedObject var viewModel: SearchResultViewModel @@ -117,6 +120,7 @@ struct Hashtag: View { } } +// MARK: - Segment Control struct SearchResultSegmentControl: View { @Binding var selectedSegment: SearchResultSegment From 1d0883f863250509baf6649aec72531c606bbc79 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 02:00:12 +0900 Subject: [PATCH 03/21] =?UTF-8?q?[#45]=20=EA=B2=80=EC=83=89=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EB=B7=B0=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- Codive/Core/Resources/TextLiteral.swift | 2 ++ .../Presentation/View/SearchResultView.swift | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Codive/Core/Resources/TextLiteral.swift b/Codive/Core/Resources/TextLiteral.swift index 93238b90..db4a5758 100644 --- a/Codive/Core/Resources/TextLiteral.swift +++ b/Codive/Core/Resources/TextLiteral.swift @@ -153,6 +153,8 @@ enum TextLiteral { static let totalCount = "총" static let countUnit = "개" static let sortAll = "전체" + static let account = "계정" + static let hashtag = "해시태그" } enum Notification { diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index ca2547e8..c312dd2c 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -38,13 +38,14 @@ struct SearchResultView: View { viewModel.handleBackTap() } ) + .padding(.horizontal, 20) .zIndex(1) .onSubmit { viewModel.executeNewSearch(query: viewModel.searchBarText) } SearchResultSegmentControl(selectedSegment: $selectedSegment) - .padding(.top, 16) + .padding(.top, 8) ScrollView { // MARK: - Account Result List @@ -63,12 +64,12 @@ struct SearchResultView: View { } else { // MARK: - Hashtag Result Grid Hashtag(viewModel: viewModel) + .padding(.horizontal, 20) } } } .navigationBarHidden(true) .background(Color.white.ignoresSafeArea(.all)) - .padding(.horizontal, 20) // MARK: - Data Loading Trigger .onAppear { viewModel.loadInitialData() @@ -127,13 +128,13 @@ struct SearchResultSegmentControl: View { var body: some View { VStack(spacing: 0) { HStack(spacing: 0) { - segmentItem(title: "계정", segment: .account) - segmentItem(title: "해시태그", segment: .hashtag) + segmentItem(title: TextLiteral.Search.account, segment: .account) + segmentItem(title: TextLiteral.Search.hashtag, segment: .hashtag) } Rectangle() - .frame(height: 1) - .foregroundStyle(Color.Codive.grayscale4) + .frame(height: 2) + .foregroundStyle(Color.Codive.grayscale6) } } @@ -144,18 +145,22 @@ struct SearchResultSegmentControl: View { } label: { VStack(spacing: 6) { Text(title) - .font(.codive_body1_medium) + .font( + selectedSegment == segment + ? .codive_body1_medium + : .codive_body1_regular + ) .foregroundStyle( selectedSegment == segment ? Color.Codive.grayscale1 - : Color.Codive.grayscale3 + : Color.Codive.grayscale4 ) Rectangle() .frame(height: 2) .foregroundStyle( selectedSegment == segment - ? Color.Codive.main0 + ? Color.Codive.point1 : .clear ) } From 0cb83e912a18d99030178a0639bc9bbe48ecf4de Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 17:58:13 +0900 Subject: [PATCH 04/21] =?UTF-8?q?[#45]=20=EC=95=8C=EB=9E=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=EC=97=90=20=EB=A7=9E=EB=8A=94=20entity=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20mock=20data=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../DataSources/NotificationDataSource.swift | 55 ++++++---- .../Domain/Entities/NotificationEntity.swift | 26 ++++- .../Component/NotificationRow.swift | 101 ++++++++++++------ .../Presentation/View/NotificationView.swift | 5 +- .../ViewModel/NotificationViewModel.swift | 6 +- .../history.imageset/Contents.json | 12 +++ ...354\225\204\354\235\264\354\275\230-2.png" | Bin 0 -> 3024 bytes .../weather.imageset/Contents.json | 12 +++ .../\354\225\204\354\235\264\354\275\230.png" | Bin 0 -> 3184 bytes 9 files changed, 150 insertions(+), 67 deletions(-) create mode 100644 Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json create mode 100644 "Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" create mode 100644 Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json create mode 100644 "Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" diff --git a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift index 9c35b2a6..4e60a3a9 100644 --- a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift +++ b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift @@ -14,34 +14,49 @@ final class NotificationDataSource { func fetchNotifications() -> [NotificationEntity] { return [ NotificationEntity( - id: 1, - imageUrl: "https://picsum.photos/id/237/200/200", - message: "홍길동님이 회원님의 옷장을 팔로우하기 시작했습니다.", - isRead: false + notificationId: 1, + notificationImageUrl: "https://picsum.photos/id/237/200/200", + notificationContent: "홍길동님이 회원님의 옷장을 팔로우하기 시작했습니다.", + redirectInfo: "user_123", + redirectType: .member, + readStatus: .unread, + createdAt: "2025-11-18T10:00:00" ), NotificationEntity( - id: 2, - imageUrl: nil, - message: "김철수님이 새로운 게시물을 업로드했습니다.", - isRead: false + notificationId: 2, + notificationImageUrl: nil, + notificationContent: "1년 전 오늘의 기록을 확인해보세요.", + redirectInfo: "post_456", + redirectType: .history, + readStatus: .unread, + createdAt: "2025-11-18T11:00:00" ), NotificationEntity( - id: 3, - imageUrl: "invalid_url", - message: "이영희님이 회원님의 게시물에 좋아요를 눌렀습니다.", - isRead: false + notificationId: 3, + notificationImageUrl: "https://picsum.photos/id/100/200/200", + notificationContent: "내일은 비가 올 예정입니다. 우산을 챙기세요!", + redirectInfo: "seoul", + redirectType: .weather, + readStatus: .read, + createdAt: "2025-11-18T12:00:00" ), NotificationEntity( - id: 4, - imageUrl: "https://picsum.photos/id/100/200/200", - message: "박민수님이 회원님의 댓글에 답글을 달았습니다.", - isRead: true + notificationId: 4, + notificationImageUrl: nil, + notificationContent: "홍길동님이 팔로우를 취소했습니다.", + redirectInfo: "user_123", + redirectType: .member, + readStatus: .unread, + createdAt: "2025-11-18T10:00:00" ), NotificationEntity( - id: 5, - imageUrl: nil, - message: "코디 추천 시즌 이벤트가 시작되었습니다.", - isRead: true + notificationId: 6, + notificationImageUrl: nil, + notificationContent: "내일은 비가 올 예정입니다. 우산을 챙기세요!", + redirectInfo: "Busan", + redirectType: .weather, + readStatus: .unread, + createdAt: "2025-11-18T12:00:00" ) ] } diff --git a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift index cfd714bf..229d9e10 100644 --- a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift +++ b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift @@ -7,9 +7,25 @@ import Foundation -struct NotificationEntity: Identifiable { - let id: Int - let imageUrl: String? - let message: String - let isRead: Bool +enum RedirectType: String, Codable { + case member = "MEMBER" + case history = "HISTORY" + case weather = "WEATHER" +} + +enum ReadStatus: String, Codable { + case read = "READ" + case unread = "UNREAD" +} + +struct NotificationEntity: Codable, Identifiable { + let notificationId: Int + let notificationImageUrl: String? + let notificationContent: String + let redirectInfo: String + let redirectType: RedirectType + let readStatus: ReadStatus + let createdAt: String + + var id: Int { notificationId } } diff --git a/Codive/Features/Notification/Presentation/Component/NotificationRow.swift b/Codive/Features/Notification/Presentation/Component/NotificationRow.swift index cd8a173e..a40915e3 100644 --- a/Codive/Features/Notification/Presentation/Component/NotificationRow.swift +++ b/Codive/Features/Notification/Presentation/Component/NotificationRow.swift @@ -8,28 +8,37 @@ import SwiftUI struct NotificationRow: View { - // MARK: - Properties - let profileImageUrl: String? - let message: String + let entity: NotificationEntity - // MARK: - Constants private let profileImageSize: CGFloat = 36 - private let messageFont = Font.codive_body2_regular - private let messageColor = Color.Codive.grayscale1 + + // MARK: - Asset Logic + /// 타입별 전용 에셋 이미지 이름 반환 + private var typeSpecificImageName: String? { + switch entity.redirectType { + case .history: + return "history" + case .weather: + return "weather" + case .member: + return nil + } + } var body: some View { HStack(spacing: 15) { - if let urlString = profileImageUrl, let url = URL(string: urlString) { + if let imageName = typeSpecificImageName { + Image(imageName) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: profileImageSize, height: profileImageSize) + .clipShape(Circle()) + } else if let urlString = entity.notificationImageUrl, let url = URL(string: urlString) { AsyncImage(url: url) { phase in if let image = phase.image { - image - .resizable() - .aspectRatio(contentMode: .fill) + image.resizable().aspectRatio(contentMode: .fill) } else if phase.error != nil { - Image(systemName: "person.circle.fill") - .resizable() - .aspectRatio(contentMode: .fill) - .foregroundStyle(Color.gray.opacity(0.3)) + defaultImage // URL 에러 시 기본 이미지 } else { ProgressView() } @@ -37,37 +46,59 @@ struct NotificationRow: View { .frame(width: profileImageSize, height: profileImageSize) .clipShape(Circle()) } else { - Image(systemName: "person.circle.fill") - .resizable() - .aspectRatio(contentMode: .fill) - .foregroundStyle(Color.gray.opacity(0.3)) - .frame(width: profileImageSize, height: profileImageSize) - .clipShape(Circle()) + defaultImage // 이미지 URL이 nil인 경우 } - Text(message) - .font(messageFont) - .foregroundStyle(messageColor) + Text(entity.notificationContent) + .font(Font.codive_body2_regular) + .foregroundStyle(Color.Codive.grayscale1) Spacer() } } + + private var defaultImage: some View { + Image(systemName: "person.circle.fill") + .resizable() + .aspectRatio(contentMode: .fill) + .foregroundStyle(Color.gray.opacity(0.3)) + .frame(width: profileImageSize, height: profileImageSize) + .clipShape(Circle()) + } } // MARK: - Preview #Preview { VStack(spacing: 16) { - NotificationRow( - profileImageUrl: "https://picsum.photos/id/237/200/200", - message: "홍길동님이 회원님의 옷장을 팔로우하기 시작했습니다." - ) - NotificationRow( - profileImageUrl: nil, - message: "김철수님이 새로운 게시물을 업로드했습니다." - ) - NotificationRow( - profileImageUrl: "invalid_url", - message: "이영희님이 회원님의 게시물에 좋아요를 눌렀습니다." - ) + NotificationRow(entity: NotificationEntity( + notificationId: 1, + notificationImageUrl: "https://picsum.photos/id/237/200/200", + notificationContent: "홍길동님이 회원님의 옷장을 팔로우하기 시작했습니다.", + redirectInfo: "user_123", + redirectType: .member, + readStatus: .unread, + createdAt: "2025-11-18T10:00:00" + )) + + NotificationRow(entity: NotificationEntity( + notificationId: 2, + notificationImageUrl: nil, + notificationContent: "김철수님이 새로운 게시물을 업로드했습니다.", + redirectInfo: "post_456", + redirectType: .history, + readStatus: .unread, + createdAt: "2025-11-18T11:00:00" + )) + + NotificationRow(entity: NotificationEntity( + notificationId: 3, + notificationImageUrl: "invalid_url", + notificationContent: "내일은 비 소식이 있습니다. 우산을 준비하세요!", + redirectInfo: "seoul", + redirectType: .weather, + readStatus: .read, + createdAt: "2025-11-18T12:00:00" + )) } + .padding() } diff --git a/Codive/Features/Notification/Presentation/View/NotificationView.swift b/Codive/Features/Notification/Presentation/View/NotificationView.swift index 030c17b5..f536093c 100644 --- a/Codive/Features/Notification/Presentation/View/NotificationView.swift +++ b/Codive/Features/Notification/Presentation/View/NotificationView.swift @@ -67,10 +67,7 @@ struct NotificationView: View { VStack(spacing: 16) { ForEach(notifications) { item in - NotificationRow( - profileImageUrl: item.imageUrl, - message: item.message - ) + NotificationRow(entity: item) } } .padding(.top, 12) diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index a18f9d8a..16e004ea 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -26,9 +26,9 @@ final class NotificationViewModel: ObservableObject { func loadData() { let allNotifications = useCase.fetchNotifications() - // isRead 상태를 기준으로 필터링 - self.unreadNotifications = allNotifications.filter { !$0.isRead } - self.readNotifications = allNotifications.filter { $0.isRead } + // readStatus Enum 값을 직접 비교하여 필터링 + self.unreadNotifications = allNotifications.filter { $0.readStatus == .unread } + self.readNotifications = allNotifications.filter { $0.readStatus == .read } } // MARK: - Navigation diff --git a/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json new file mode 100644 index 00000000..060462d4 --- /dev/null +++ b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "아이콘-2.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" "b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" new file mode 100644 index 0000000000000000000000000000000000000000..4a18d2b55e2956714ee41d7452ab01a2e8d76790 GIT binary patch literal 3024 zcma);`9BkmAICRFGv>&KNsdvjC5x0kjxDot6LR0j)}bv4C7LsG)M8z3xt6OrBGd;t zipb1Sa(tb+DYN-7`uh9_pU30?(K@_n03c##C3c7V~ zd!()({o@2Lws49*r`>H}UU_`A`aLu+}Ih}lR7DP<+O?h>@%vD0Ry_%vB?D2dX?=_mE$zuz`avM*--CEpw^x+S;XXI<1 zH%x<%I>I1?l3eW$J)89S0=*5lK8X=BF_$`QZ2f4q>gx8oO$PyvmWAUnz43R)Dc>_Q zMwA%;yFc2{Zq#WUTJ2Bs;OvMHX-*f=;~jsOq`54~TJpiFtl7SHI0rCre0Pr*JlP^O z>+WM0BX)nvrfy)B<5R*f83#(6E*@j4Tmp~dmh0aC%=SpkS`c8*W&d-V_Xg-UU3@Gc ztj?4fZaxovH75(Xiuld=wY}gki9CK4I4AK)QuJGpVM+b@Ak}_zCfic@eQ$7@y4F<* zQ_VbgYFFFa{iKP+0EKQLI!P}WS4cF46;V3jVt3^3{aCjdowS5JYg+oThkj+3fqZC< zG5Ofck6-|}xpLXpvA`B-e}3a~6MVmOy|2!&oe*&W;fYBYlETZq?s8Edy>SK+#3n`{ zUfD@IJx)fOej|c(*+RQ6zYWxKM2}Cb!EYb&E>}?~Az2*>37a=GIOS??iu&XZn`ucFOgRwJAcfIM5EKSYICyz3zot zG1~uWCS2SSAQ<)1Xu0R>eAq^Pt7lQi^gl7eYGv#BpEeg3w&g9Or?|gY1N=fv@1Lqr zzqQy`VHm1{G77)Mn}Jt6@QOO%{9YxIy(*;?zrgB&^4lYvLKaG%0`gi&-3K# zK2B~fdAZbq!(+c3Qj_oo&Ipn=(2o82&C+Pf=_Ugxv91I=Nq}||h#HIb>$TwHiZ|H# zA3%;diaA7z4SfrC5eqwrbtWA9qPJ zQYKz`}f{c7$6tF|GL|t z7PospCEsyPCkN&!P*~wPonn&S3;A9H%2x47tr;zQ!C8qO5CaWFji8P}-`NIJ)1`lp z)7mpBf~WNsBdi2_b-)U7eYNNW$H>agW}#1%E9A~&<_eSzYy(TCZiC+@Tv9w?(v9w? z%~fWNboAFum3NXY`3GZLKOZ-Y&&~tlESORQ z%DRC&k#n&;Tpr}jy#Lj7;q#C$gpdxKF1?d{OFxX(gBGH}YS4L|C6mcyDhHW|qs#g^ zpLtqijh^ZDbjd7xZD9W#c)8-2Iv7=&>^dqlM|Z1G5G$0~M9<`d`OlvGp_}~Yt0d3a zhDlacGO8L;L@}GM{{j>HznzLIO?*${m^}QXvFnuxO2W*DM<@C!$pU?(c7BS>Tz+x` zhmlZU?wTYg;xP?Q938Ng&APuU)LiAaiely(sr;pF?wnkMoJE0@pR_~?Q-8b?EWd$N z?ETA8d?kGC&KPt0lWClO&OxZZ6 z!mdTHRuwuHg-x{4OeB#i$0eq&o<7a@Px%VlS+du_4pTpYyC{N`od7#n9Qj#O&GJK1 zoy-X3Nv9wZ?{K%W}8FIr(@V{c}id&<@~Qki*3JiHen^2W6xBrZMfw^|CoN)|d?_4m>-$8n zEXS!e80E6q^9hN}dfAmGvStkpbd&q?j^>1k9Rrn7Lr?qvWTMD8U4xrV{C>W76NYYn ztn2u7PrEfj|B#1kn_Q!7oEpykc!5vZ2gZ^3Mi?bVicntN9pU#-F)RReVr=K>?G9>8 zMb`t>G3z2bN?`q5CEp>guwjD!x6dpOh1?;NKDO!k$dGWJQ26mg%FD3%5wh71MMuhN zYSksEq2(Pf0Ay4`Ghod&#L+Jnb{hx2hkS@c<fC{nA@|t2;PzGa@I3t|mtnJqj z(bLfTP09VQS#NIqyS7qIjsUmu0`x9}$Sp_{J_bcgb7FM#GPnqd^3sVR1;Tq;py0!i z>~&yG*HML2J0kmgEvWH#btFV@iC}5k8oi@h&xfeD>(U z=a$)*Ndmr$I$yYIyQ*v9?s2@;Uz;=cG2SsYKFZZr0UxjZ9dWg2frYSggWWmh{`N@6 z;Avz|I^A&JBOhc)wr9i}!IwlfkJRMa8Q6{c!cr-M+4Z)#7ozRo8_PJ!wIZBTrSqSU zK~DTpWA$@Uogt4dlEp%DNS+_=-rk(KR#nqX#f*5x?roB)?t>PRo2BeiEpGWl4N>+W zr$QxI-TOs*yjcJeDJvEt|E2B5{~oM_d8wZrN=jRT8j6{kkG}joz17D+%#1VaAc-<= zIBo40vy#3NPkPp~5IZiN<4=+<8Q>i3zuh69JJ)h02{b`j>Cc>Kud15hZPl%-IRb!+ zQ}=d~z4nLpC#{^IGLMKICb%Qf_}L_!vOA8iZnX^6j_?n7+OJrz=v6;hdT>3d*apAi zve`0!!=vonf^p~~KF=)qN&~Pl?tDg*j8txU*ij19llKXqAjU=T#GEeRA4aVLbUL?GwtwZ6tKrWC-K2ZRx4(Y)0wXbojG z4pSfQ+f2?C;IsV>^;W%p%Asd0i|Wx_gBq!}z4_sy57RwPW>D~XUCuS$!zbFAL#UIw z0X`~Y_ku&J1q75w9lr&5xBWs?R_=>-uDVZB_+|>MP500Vsmz;d{-ASTH(>2M?afrx ztPw^7!hJ)dr%jlMvx~#A;8D3h?pl-JpO@WnS3tT*PbGSqU|l9R%lUeSHAXq%bMr9F z={nuEwpHxtYd5XsXvzSD7}xW?Ir=!h@EFf8mR0zdm@-r5RcLzd`6f%|yh(;9W?g5+ z&>^hr5$%Ug8CD+k(r2zfg!UvPaBhK__(HOOH>J8;%cYc(Ht2A&u{^a>tUs?Ql7Cz! oTGTzBReeF`zhbO-To?~fn`-Agq191+0N?;KQ)^u1dGDD218s|$D*ylh literal 0 HcmV?d00001 diff --git a/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json new file mode 100644 index 00000000..7f1dafbc --- /dev/null +++ b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "아이콘.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" "b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" new file mode 100644 index 0000000000000000000000000000000000000000..e048d964632b7454964039800442c9c936616346 GIT binary patch literal 3184 zcmaJ^`8O1b7oQnpUuSF~Vu;BwWGf~aV-PVWCcBKSvL=+}NX-@a+p5Bz)6g}WkFeuxASqM_THtzK!FR?(dA!A^eJ;yc5I=Unx)gj zu}Y$y(Q7+ugUR!c3T6$t2EY0Y!%Qo{*w*t|3vx&G6jxq#ka-zfMyWK!Iv?zV@%7p& z*loGL+2J}fnoJDIW{xqlE*OMw*V!Gf81|M?NX#tpP}L7QiAp)Y9nf{my_JO$-l#}c z|3*PO;Raf&E=%U+%Oh=(j}xxn4$7lpdgN?tZ{X9g@+yCL=FMDVf&;K%JR&CZ7NXmC zP}*gB^vot_km=O--Nzh6X;M{_i_Ptf8CL!ijrU8Rx_e#4(kE(W*y;e=D@JVtwo{Armn0oih@jzy4fO(JluG*xtX*ggDy$WJkzHA7K))Bb(Xsz0 zisIt0{X*WD;ZbA_G9M1fU_2;TWXV_na##F!4ky9Bdbh^#!pdi5LtLeree2hkEtI21 zmRZPh_6>IjL@ijmz#4@4J-F#&1hmu|c>JE$?JN+mOT!d{V*-k2no8XYZ%qoO zRq&T`i&mqL)>uW|YM|d~#x=5@hG2z63fGlW3A#e&$+VdxwQ{lszqY-r1MM%uj)4y$ z(mIx8ZsE&dn3q9Su)Zfk8lCt^(9RJ|$|2Lt%FW?hNtA?Ik*GX-we_ zdcgIQyCd=^1 z`u9qWJrzoQy(4pr!r|wJ-xQ&E_7?AkW;)98V8mQJ6+z0tkfH0*lhNGdSzSSPIi+Z> zQUzkOPqMj<8>SwqU<1D^3e4cTSFE$ulH9b$?7IBS_DvI4DK-Ux($j5ht4*Gft(%v0 zwmi!|Yzih|;QLQdbWVj>mDi!Q7yxR;#Gt2?t92=X?Y%T!7UE&3YEg$LWWQZM ztf90SSAi1c%1Q#+1fDe9(v;wm2Px~aB@9tgVMvQpypR-iF_^5I3KrdJA4`gPBLw93 zkOM*6F(=z>>;?XGeXW;V6`5C98bkW_pz`lv@(?<|L;x0l1HJJ{lOim_pjv;CjD`CE zy+p09nL3f#iP}QfYCv5c;dnG9{r*sW&vb$^vhX&$=j$Ke`7%pfiL8*n96neTa~(s8_3;S~gasd~5EQ$O zGCz8>`gv;)fs2S)MU)Y1vD;Kmmd++#AiNfdN4oPbIKcs*hTvq_52Hh3^N%-VMvfKcp-* zze}*Cvt?CZwtL0T7m)Box5AA#udkU)Imj`pwh&8V-0&6ofK1v!pa z7@QFt@*kQ6gG2Z?q%sVsr93jmcrDf?=1a2lHKqM#9vds*SIF^-|2JkKm${m3?_An| zJwT#tnlSMj|&%3v;jt(N|}mc*SWV|zl^{Jw7iL!SHkxn}QCNnN6*N^J~XB z8F27imyu{-RE3}}0*g|Rlm-YZXbpU-prxZUv_uy7V6MkPgV@5ms&_v)-%#IvG?6*H z1P~dlDBo6kP`~{vEPnujVjOw(Mk9#M3@S-H+sOPX(|RxCZ|W&U>_{?b}YXF@)X zhIkh2B=PCt4}#exY>3!Wb|r$%?q_^s9j7tUo|A2+bz(?)p@AOvmfFK#+B@L*r0?UM zauxl&8f{pXu^Cfkan0pZ1ny8w`AUsgZ0AtIqO|V*L0FVh#XThdJQKbrU5@V=u7;Zw ziU~cvie>A}rH<>Grz#916R>GVoQ{s;`{B4WTbE1N9T3a}GKlKba0sFNIXtwHp~nxj zl8Y&Cx}v7^VjrKJ?AR9FUoOafebpS=yXZJ(zyX6UlmQvguCJWnQ2zW3C{n9K3~fe* zH*;zi1c6{T-I8)fp4$livtD(%C>7MeVI$)2JUZw2XMy4?Xv3+jO7-Pvx$Fb6m@oD? zBXC;59z{sKW;f$(!wYP(Ix&S_HQqBHT`}jqHWCd{+WyJvRTI%t1GJAIUEW&Zfqnb~ z&PjI>3C*o@0bet8l2(10JAwyixp$^LE{eDOU&k%Wg*mrt;C3nkCG$(#>*PTIm>J`+ IbPSRFKk|9hrT_o{ literal 0 HcmV?d00001 From 7a85bbeacd0d3fe90952232c4f4524c6524e3e3a Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 21:53:50 +0900 Subject: [PATCH 05/21] =?UTF-8?q?[#45]=20=EC=8B=A0=EA=B3=A0=20=EC=A0=91?= =?UTF-8?q?=EC=88=98=20=EC=95=88=EB=82=B4=20component=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Component/ReportSubmissionGuide.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift diff --git a/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift new file mode 100644 index 00000000..1a8baa43 --- /dev/null +++ b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift @@ -0,0 +1,64 @@ +// +// ReportSubmissionGuide.swift +// Codive +// +// Created by 한금준 on 12/30/25. +// + +import SwiftUI + +// MARK: - 신고 접수 안내 컴포넌트 + +struct ReportSubmissionGuide: View { + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .center, spacing: 8) { + Image("warning") + .resizable() + .renderingMode(.template) + .frame(width: 24, height: 24) + .foregroundColor(Color.Codive.point1) + + // 텍스트 영역 + VStack(alignment: .leading, spacing: 12) { + // 제목 + Text("신고 접수 안내") + .font(.codive_body1_medium) + .foregroundColor(Color.Codive.grayscale1) + } + + Spacer() + } + + VStack(alignment: .leading, spacing: 4) { + Text("회원님의 게시글이 운영 정책 위반으로 신고되었습니다.") + .font(.codive_body2_regular) + .foregroundColor(Color.Codive.grayscale1) + + Text("확인 및 조치는 영업일 기준 3~5일정도 소요됩니다.") + .font(.codive_body2_regular) + .foregroundColor(Color.Codive.grayscale1) + } + } + .padding(.horizontal, 16) + .padding(.top, 16) + .padding(.bottom, 20) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.Codive.point4) + ) + } +} + +// MARK: - Preview + +#Preview { + ZStack { + Color.white.ignoresSafeArea() + + ReportSubmissionGuide() + .padding(.horizontal, 20) + } +} From f9b234f3acbd192be8796fbaee41fe21b19fc940 Mon Sep 17 00:00:00 2001 From: Funital Date: Tue, 30 Dec 2025 22:02:48 +0900 Subject: [PATCH 06/21] =?UTF-8?q?[#45]=20=EC=8B=A0=EA=B3=A0=20=EC=A0=91?= =?UTF-8?q?=EC=88=98=20=EC=95=88=EB=82=B4=20=EB=9D=84=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Data/DataSources/NotificationDataSource.swift | 4 ++++ .../Data/Repositories/NotificationRepositoryImpl.swift | 4 ++++ .../Notification/Domain/Entities/NotificationEntity.swift | 4 ++++ .../Domain/Protocols/NotificationRepository.swift | 1 + .../Notification/Domain/UseCases/NotificationUseCase.swift | 4 ++++ .../Notification/Presentation/View/NotificationView.swift | 6 +++++- .../Presentation/ViewModel/NotificationViewModel.swift | 6 ++++-- 7 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift index 4e60a3a9..a90da23d 100644 --- a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift +++ b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift @@ -60,4 +60,8 @@ final class NotificationDataSource { ) ] } + + func fetchReportStatus() -> ReportEntity { + return ReportEntity(isReported: true) + } } diff --git a/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift b/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift index 46483b88..69c675e1 100644 --- a/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift +++ b/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift @@ -18,4 +18,8 @@ final class NotificationRepositoryImpl: NotificationRepository { func fetchNotifications() -> [NotificationEntity] { return datasource.fetchNotifications() } + + func fetchReportStatus() -> ReportEntity { + return datasource.fetchReportStatus() + } } diff --git a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift index 229d9e10..d5d407bf 100644 --- a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift +++ b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift @@ -29,3 +29,7 @@ struct NotificationEntity: Codable, Identifiable { var id: Int { notificationId } } + +struct ReportEntity: Codable { + let isReported: Bool +} diff --git a/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift b/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift index 6238b6a6..b0a5c226 100644 --- a/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift +++ b/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift @@ -7,4 +7,5 @@ protocol NotificationRepository { func fetchNotifications() -> [NotificationEntity] + func fetchReportStatus() -> ReportEntity } diff --git a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift index ec3ce227..8c661928 100644 --- a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift +++ b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift @@ -18,4 +18,8 @@ final class NotificationUseCase { func fetchNotifications() -> [NotificationEntity] { return repository.fetchNotifications() } + + func fetchReportStatus() -> ReportEntity { + return repository.fetchReportStatus() + } } diff --git a/Codive/Features/Notification/Presentation/View/NotificationView.swift b/Codive/Features/Notification/Presentation/View/NotificationView.swift index f536093c..345eac5c 100644 --- a/Codive/Features/Notification/Presentation/View/NotificationView.swift +++ b/Codive/Features/Notification/Presentation/View/NotificationView.swift @@ -25,7 +25,11 @@ struct NotificationView: View { } ScrollView { - VStack { + VStack(spacing: 0) { + if viewModel.isReported { + ReportSubmissionGuide() + } + // MARK: - Notification Sections notificationSection( title: TextLiteral.Notification.notRead, diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 16e004ea..47ca1e17 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -16,6 +16,8 @@ final class NotificationViewModel: ObservableObject { @Published var unreadNotifications: [NotificationEntity] = [] @Published var readNotifications: [NotificationEntity] = [] + @Published var isReported: Bool = false + // MARK: - Initializer init(navigationRouter: NavigationRouter, useCase: NotificationUseCase) { self.navigationRouter = navigationRouter @@ -25,10 +27,10 @@ final class NotificationViewModel: ObservableObject { // MARK: - Methods func loadData() { let allNotifications = useCase.fetchNotifications() - - // readStatus Enum 값을 직접 비교하여 필터링 self.unreadNotifications = allNotifications.filter { $0.readStatus == .unread } self.readNotifications = allNotifications.filter { $0.readStatus == .read } + + self.isReported = useCase.fetchReportStatus().isReported } // MARK: - Navigation From 13713bc599a14725457c70cf16728c58a26e2b13 Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 00:13:52 +0900 Subject: [PATCH 07/21] =?UTF-8?q?[#45]=20=EC=8B=A0=EA=B3=A0=20=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20entity=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../DataSources/NotificationDataSource.swift | 2 +- .../Domain/Entities/NotificationEntity.swift | 6 +++++ .../Component/ReportSubmissionGuide.swift | 26 ++++++++++++------- .../Presentation/View/NotificationView.swift | 4 +-- .../ViewModel/NotificationViewModel.swift | 5 +++- 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift index a90da23d..07f36c9d 100644 --- a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift +++ b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift @@ -62,6 +62,6 @@ final class NotificationDataSource { } func fetchReportStatus() -> ReportEntity { - return ReportEntity(isReported: true) + return ReportEntity(isReported: true, reportType: .feed) } } diff --git a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift index d5d407bf..a9ff9d14 100644 --- a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift +++ b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift @@ -30,6 +30,12 @@ struct NotificationEntity: Codable, Identifiable { var id: Int { notificationId } } +enum ReportType: String, Codable { + case feed = "FEED" + case comment = "COMMENT" +} + struct ReportEntity: Codable { let isReported: Bool + let reportType: ReportType? } diff --git a/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift index 1a8baa43..a604d84e 100644 --- a/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift +++ b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift @@ -7,9 +7,18 @@ import SwiftUI -// MARK: - 신고 접수 안내 컴포넌트 - struct ReportSubmissionGuide: View { + let reportType: ReportType + + // 타입에 따른 안내 문구 결정 + private var reportTargetText: String { + switch reportType { + case .feed: + return "게시글이" + case .comment: + return "댓글이" + } + } var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -20,19 +29,17 @@ struct ReportSubmissionGuide: View { .frame(width: 24, height: 24) .foregroundColor(Color.Codive.point1) - // 텍스트 영역 VStack(alignment: .leading, spacing: 12) { - // 제목 Text("신고 접수 안내") .font(.codive_body1_medium) .foregroundColor(Color.Codive.grayscale1) } - Spacer() } VStack(alignment: .leading, spacing: 4) { - Text("회원님의 게시글이 운영 정책 위반으로 신고되었습니다.") + // 동적 텍스트 적용 + Text("회원님의 \(reportTargetText) 운영 정책 위반으로 신고되었습니다.") .font(.codive_body2_regular) .foregroundColor(Color.Codive.grayscale1) @@ -55,10 +62,11 @@ struct ReportSubmissionGuide: View { // MARK: - Preview #Preview { - ZStack { - Color.white.ignoresSafeArea() + VStack { + ReportSubmissionGuide(reportType: .comment) + .padding(.horizontal, 20) - ReportSubmissionGuide() + ReportSubmissionGuide(reportType: .feed) .padding(.horizontal, 20) } } diff --git a/Codive/Features/Notification/Presentation/View/NotificationView.swift b/Codive/Features/Notification/Presentation/View/NotificationView.swift index 345eac5c..edd666a6 100644 --- a/Codive/Features/Notification/Presentation/View/NotificationView.swift +++ b/Codive/Features/Notification/Presentation/View/NotificationView.swift @@ -26,8 +26,8 @@ struct NotificationView: View { ScrollView { VStack(spacing: 0) { - if viewModel.isReported { - ReportSubmissionGuide() + if viewModel.isReported, let type = viewModel.reportType { + ReportSubmissionGuide(reportType: type) } // MARK: - Notification Sections diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 47ca1e17..9d9f7aaf 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -17,6 +17,7 @@ final class NotificationViewModel: ObservableObject { @Published var readNotifications: [NotificationEntity] = [] @Published var isReported: Bool = false + @Published var reportType: ReportType? = nil // MARK: - Initializer init(navigationRouter: NavigationRouter, useCase: NotificationUseCase) { @@ -30,7 +31,9 @@ final class NotificationViewModel: ObservableObject { self.unreadNotifications = allNotifications.filter { $0.readStatus == .unread } self.readNotifications = allNotifications.filter { $0.readStatus == .read } - self.isReported = useCase.fetchReportStatus().isReported + let reportStatus = useCase.fetchReportStatus() + self.isReported = reportStatus.isReported + self.reportType = reportStatus.reportType } // MARK: - Navigation From b63815f3f480550b2fc04ba20c53e3bb54c626da Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 00:17:05 +0900 Subject: [PATCH 08/21] =?UTF-8?q?[#45]=20=EA=B2=BD=EA=B3=A0=20=EC=97=86?= =?UTF-8?q?=EC=95=A0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Presentation/ViewModel/NotificationViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 9d9f7aaf..30a19035 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -17,7 +17,7 @@ final class NotificationViewModel: ObservableObject { @Published var readNotifications: [NotificationEntity] = [] @Published var isReported: Bool = false - @Published var reportType: ReportType? = nil + @Published var reportType: ReportType? // MARK: - Initializer init(navigationRouter: NavigationRouter, useCase: NotificationUseCase) { From da5485571b58075281fb044b8ed4d12fdae1804a Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 00:37:10 +0900 Subject: [PATCH 09/21] =?UTF-8?q?[#45]=20=EC=9D=BD=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C=20=EC=95=8C=EB=9E=8C=20=EC=84=A0=ED=83=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Presentation/View/NotificationView.swift | 7 ++++ .../ViewModel/NotificationViewModel.swift | 33 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Codive/Features/Notification/Presentation/View/NotificationView.swift b/Codive/Features/Notification/Presentation/View/NotificationView.swift index edd666a6..e33f0c53 100644 --- a/Codive/Features/Notification/Presentation/View/NotificationView.swift +++ b/Codive/Features/Notification/Presentation/View/NotificationView.swift @@ -72,6 +72,13 @@ struct NotificationView: View { VStack(spacing: 16) { ForEach(notifications) { item in NotificationRow(entity: item) + .contentShape(Rectangle()) // 투명한 영역도 탭이 되도록 설정 + .onTapGesture { + if item.readStatus == .unread { + viewModel.markAsRead(notificationId: item.notificationId) + } + // 페이지 이동 로직 호출 구간 + } } } .padding(.top, 12) diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 30a19035..1283964c 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -27,15 +27,42 @@ final class NotificationViewModel: ObservableObject { // MARK: - Methods func loadData() { - let allNotifications = useCase.fetchNotifications() - self.unreadNotifications = allNotifications.filter { $0.readStatus == .unread } - self.readNotifications = allNotifications.filter { $0.readStatus == .read } + updateNotificationLists(useCase.fetchNotifications()) let reportStatus = useCase.fetchReportStatus() self.isReported = reportStatus.isReported self.reportType = reportStatus.reportType } + /// 알림 클릭 시 읽음 처리 로직 + func markAsRead(notificationId: Int) { + // 1. 읽지 않은 알림 목록에서 해당 아이템 찾기 + if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { + // 2. 해당 아이템 추출 및 상태 변경 (실제 앱에선 여기서 API 호출을 수행합니다) + let readItem = unreadNotifications.remove(at: index) + + // 3. 상태가 변경된 새 객체 생성 (Entity가 struct이므로 새로 생성) + let updatedItem = NotificationEntity( + notificationId: readItem.notificationId, + notificationImageUrl: readItem.notificationImageUrl, + notificationContent: readItem.notificationContent, + redirectInfo: readItem.redirectInfo, + redirectType: readItem.redirectType, + readStatus: .read, // 읽음으로 변경 + createdAt: readItem.createdAt + ) + + // 4. 읽음 목록 상단에 추가 및 UI 업데이트 + readNotifications.insert(updatedItem, at: 0) + } + } + + /// 알림 목록을 필터링하여 unread/read로 나누는 공통 로직 + private func updateNotificationLists(_ all: [NotificationEntity]) { + self.unreadNotifications = all.filter { $0.readStatus == .unread } + self.readNotifications = all.filter { $0.readStatus == .read } + } + // MARK: - Navigation func handleBackTap() { navigationRouter.navigateBack() From a7ff22a1febe2216daf7bd421731986179c97225 Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 00:56:16 +0900 Subject: [PATCH 10/21] =?UTF-8?q?[#45]=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20=EB=A1=9C=EC=A7=81=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 #45 --- .../DataSources/NotificationDataSource.swift | 4 ++ .../NotificationRepositoryImpl.swift | 4 ++ .../Domain/Entities/NotificationEntity.swift | 10 +++++ .../Protocols/NotificationRepository.swift | 1 + .../Domain/UseCases/NotificationUseCase.swift | 9 ++++ .../ViewModel/NotificationViewModel.swift | 42 +++++++++---------- 6 files changed, 48 insertions(+), 22 deletions(-) diff --git a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift index 07f36c9d..ff54519c 100644 --- a/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift +++ b/Codive/Features/Notification/Data/DataSources/NotificationDataSource.swift @@ -64,4 +64,8 @@ final class NotificationDataSource { func fetchReportStatus() -> ReportEntity { return ReportEntity(isReported: true, reportType: .feed) } + + func patchNotificationRead(notificationId: Int) async throws { + print("서버에 알림 \(notificationId)번 읽음 처리 요청 전송") + } } diff --git a/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift b/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift index 69c675e1..032b07ee 100644 --- a/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift +++ b/Codive/Features/Notification/Data/Repositories/NotificationRepositoryImpl.swift @@ -22,4 +22,8 @@ final class NotificationRepositoryImpl: NotificationRepository { func fetchReportStatus() -> ReportEntity { return datasource.fetchReportStatus() } + + func markNotificationAsRead(request: NotificationReadRequestEntity) async throws { + try await datasource.patchNotificationRead(notificationId: request.notificationId) + } } diff --git a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift index a9ff9d14..a8ea5be6 100644 --- a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift +++ b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift @@ -7,17 +7,20 @@ import Foundation +/// 알림 유형 enum RedirectType: String, Codable { case member = "MEMBER" case history = "HISTORY" case weather = "WEATHER" } +/// 알림 읽음/읽지 않음 유형 enum ReadStatus: String, Codable { case read = "READ" case unread = "UNREAD" } +/// 알림 목록 조회 api struct NotificationEntity: Codable, Identifiable { let notificationId: Int let notificationImageUrl: String? @@ -30,12 +33,19 @@ struct NotificationEntity: Codable, Identifiable { var id: Int { notificationId } } +/// 신고 접수 유형 enum ReportType: String, Codable { case feed = "FEED" case comment = "COMMENT" } +/// 신고 접수 안내 api struct ReportEntity: Codable { let isReported: Bool let reportType: ReportType? } + +/// 알림 읽음 처리 request api +struct NotificationReadRequestEntity: Codable { + let notificationId: Int +} diff --git a/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift b/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift index b0a5c226..eb3395be 100644 --- a/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift +++ b/Codive/Features/Notification/Domain/Protocols/NotificationRepository.swift @@ -8,4 +8,5 @@ protocol NotificationRepository { func fetchNotifications() -> [NotificationEntity] func fetchReportStatus() -> ReportEntity + func markNotificationAsRead(request: NotificationReadRequestEntity) async throws } diff --git a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift index 8c661928..ec0ebe67 100644 --- a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift +++ b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift @@ -22,4 +22,13 @@ final class NotificationUseCase { func fetchReportStatus() -> ReportEntity { return repository.fetchReportStatus() } + + func markNotificationAsRead(notificationId: Int) async { + let request = NotificationReadRequestEntity(notificationId: notificationId) + do { + try await repository.markNotificationAsRead(request: request) + } catch { + print("알림 읽음 처리 실패: \(error)") + } + } } diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 1283964c..5405ee82 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -33,31 +33,29 @@ final class NotificationViewModel: ObservableObject { self.isReported = reportStatus.isReported self.reportType = reportStatus.reportType } - - /// 알림 클릭 시 읽음 처리 로직 + func markAsRead(notificationId: Int) { - // 1. 읽지 않은 알림 목록에서 해당 아이템 찾기 - if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { - // 2. 해당 아이템 추출 및 상태 변경 (실제 앱에선 여기서 API 호출을 수행합니다) - let readItem = unreadNotifications.remove(at: index) - - // 3. 상태가 변경된 새 객체 생성 (Entity가 struct이므로 새로 생성) - let updatedItem = NotificationEntity( - notificationId: readItem.notificationId, - notificationImageUrl: readItem.notificationImageUrl, - notificationContent: readItem.notificationContent, - redirectInfo: readItem.redirectInfo, - redirectType: readItem.redirectType, - readStatus: .read, // 읽음으로 변경 - createdAt: readItem.createdAt - ) - - // 4. 읽음 목록 상단에 추가 및 UI 업데이트 - readNotifications.insert(updatedItem, at: 0) + Task { + await useCase.markNotificationAsRead(notificationId: notificationId) + + if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { + let readItem = unreadNotifications.remove(at: index) + + let updatedItem = NotificationEntity( + notificationId: readItem.notificationId, + notificationImageUrl: readItem.notificationImageUrl, + notificationContent: readItem.notificationContent, + redirectInfo: readItem.redirectInfo, + redirectType: readItem.redirectType, + readStatus: .read, + createdAt: readItem.createdAt + ) + + readNotifications.insert(updatedItem, at: 0) + } } } - - /// 알림 목록을 필터링하여 unread/read로 나누는 공통 로직 + private func updateNotificationLists(_ all: [NotificationEntity]) { self.unreadNotifications = all.filter { $0.readStatus == .unread } self.readNotifications = all.filter { $0.readStatus == .read } From 2fc262611221e819d6b72da5daed4a3e511e2551 Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 01:02:07 +0900 Subject: [PATCH 11/21] =?UTF-8?q?[#45]=20padding=20=EA=B0=92=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Notification/Presentation/View/NotificationView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Codive/Features/Notification/Presentation/View/NotificationView.swift b/Codive/Features/Notification/Presentation/View/NotificationView.swift index e33f0c53..4e0cefa1 100644 --- a/Codive/Features/Notification/Presentation/View/NotificationView.swift +++ b/Codive/Features/Notification/Presentation/View/NotificationView.swift @@ -67,7 +67,7 @@ struct NotificationView: View { .foregroundStyle(Color.Codive.grayscale3) Spacer() } - .padding(.top, 32) + .padding(.top, 20) VStack(spacing: 16) { ForEach(notifications) { item in From 9aa7267f18543dc72638ef5a593477300b7f7a8d Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 12:44:49 +0900 Subject: [PATCH 12/21] =?UTF-8?q?[#45]=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Domain/UseCases/NotificationUseCase.swift | 8 +--- .../ViewModel/NotificationViewModel.swift | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift index ec0ebe67..2690eec9 100644 --- a/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift +++ b/Codive/Features/Notification/Domain/UseCases/NotificationUseCase.swift @@ -23,12 +23,8 @@ final class NotificationUseCase { return repository.fetchReportStatus() } - func markNotificationAsRead(notificationId: Int) async { + func markNotificationAsRead(notificationId: Int) async throws { let request = NotificationReadRequestEntity(notificationId: notificationId) - do { - try await repository.markNotificationAsRead(request: request) - } catch { - print("알림 읽음 처리 실패: \(error)") - } + try await repository.markNotificationAsRead(request: request) } } diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 5405ee82..5ea3e49c 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -18,6 +18,7 @@ final class NotificationViewModel: ObservableObject { @Published var isReported: Bool = false @Published var reportType: ReportType? + @Published var readErrorMessage: String? // MARK: - Initializer init(navigationRouter: NavigationRouter, useCase: NotificationUseCase) { @@ -33,29 +34,34 @@ final class NotificationViewModel: ObservableObject { self.isReported = reportStatus.isReported self.reportType = reportStatus.reportType } - + func markAsRead(notificationId: Int) { Task { - await useCase.markNotificationAsRead(notificationId: notificationId) - - if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { - let readItem = unreadNotifications.remove(at: index) + do { + try await useCase.markNotificationAsRead(notificationId: notificationId) - let updatedItem = NotificationEntity( - notificationId: readItem.notificationId, - notificationImageUrl: readItem.notificationImageUrl, - notificationContent: readItem.notificationContent, - redirectInfo: readItem.redirectInfo, - redirectType: readItem.redirectType, - readStatus: .read, - createdAt: readItem.createdAt - ) - - readNotifications.insert(updatedItem, at: 0) + if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { + let readItem = unreadNotifications.remove(at: index) + + let updatedItem = NotificationEntity( + notificationId: readItem.notificationId, + notificationImageUrl: readItem.notificationImageUrl, + notificationContent: readItem.notificationContent, + redirectInfo: readItem.redirectInfo, + redirectType: readItem.redirectType, + readStatus: .read, + createdAt: readItem.createdAt + ) + + readNotifications.insert(updatedItem, at: 0) + } + } catch { + readErrorMessage = "알림 읽음 처리에 실패했어요. 잠시 후 다시 시도해 주세요." + print("알림 읽음 처리 실패: \(error)") } } } - + private func updateNotificationLists(_ all: [NotificationEntity]) { self.unreadNotifications = all.filter { $0.readStatus == .unread } self.readNotifications = all.filter { $0.readStatus == .read } From cce88a86c38feb2802e80a365a3714f73b3e5236 Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 12:52:57 +0900 Subject: [PATCH 13/21] =?UTF-8?q?[#45]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20VStack=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- Codive/Core/Resources/TextLiteral.swift | 7 +++++++ .../Component/ReportSubmissionGuide.swift | 19 ++++++++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Codive/Core/Resources/TextLiteral.swift b/Codive/Core/Resources/TextLiteral.swift index db4a5758..c772aa3c 100644 --- a/Codive/Core/Resources/TextLiteral.swift +++ b/Codive/Core/Resources/TextLiteral.swift @@ -162,6 +162,13 @@ enum TextLiteral { static let read = "읽음" static let notRead = "읽지 않음" static let noNewNoti = "새로운 알림이 없습니다." + static let feedType = "게시글" + static let commentType = "댓글" + static let reportTitle = "신고 접수 안내" + static func reportBody1(_ target: String) -> String { + "회원님의 \(target)이 운영 정책 위반으로 신고되었습니다." + } + static let reportBody2 = "확인 및 조치는 영업일 기준 3~5일정도 소요됩니다." } enum LookBook { diff --git a/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift index a604d84e..b2509f4f 100644 --- a/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift +++ b/Codive/Features/Notification/Presentation/Component/ReportSubmissionGuide.swift @@ -10,13 +10,12 @@ import SwiftUI struct ReportSubmissionGuide: View { let reportType: ReportType - // 타입에 따른 안내 문구 결정 private var reportTargetText: String { switch reportType { case .feed: - return "게시글이" + return TextLiteral.Notification.feedType case .comment: - return "댓글이" + return TextLiteral.Notification.commentType } } @@ -29,21 +28,19 @@ struct ReportSubmissionGuide: View { .frame(width: 24, height: 24) .foregroundColor(Color.Codive.point1) - VStack(alignment: .leading, spacing: 12) { - Text("신고 접수 안내") - .font(.codive_body1_medium) - .foregroundColor(Color.Codive.grayscale1) - } + Text(TextLiteral.Notification.reportTitle) + .font(.codive_body1_medium) + .foregroundColor(Color.Codive.grayscale1) + Spacer() } VStack(alignment: .leading, spacing: 4) { - // 동적 텍스트 적용 - Text("회원님의 \(reportTargetText) 운영 정책 위반으로 신고되었습니다.") + Text(TextLiteral.Notification.reportBody1(reportTargetText)) .font(.codive_body2_regular) .foregroundColor(Color.Codive.grayscale1) - Text("확인 및 조치는 영업일 기준 3~5일정도 소요됩니다.") + Text(TextLiteral.Notification.reportBody2) .font(.codive_body2_regular) .foregroundColor(Color.Codive.grayscale1) } From d5d23903823da879913d9c515077c4754838d9b7 Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 12:57:57 +0900 Subject: [PATCH 14/21] =?UTF-8?q?[#45]=20=EC=9D=BD=EC=9D=8C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B0=B1=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Domain/Entities/NotificationEntity.swift | 2 +- .../ViewModel/NotificationViewModel.swift | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift index a8ea5be6..c720c5a7 100644 --- a/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift +++ b/Codive/Features/Notification/Domain/Entities/NotificationEntity.swift @@ -27,7 +27,7 @@ struct NotificationEntity: Codable, Identifiable { let notificationContent: String let redirectInfo: String let redirectType: RedirectType - let readStatus: ReadStatus + var readStatus: ReadStatus let createdAt: String var id: Int { notificationId } diff --git a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift index 5ea3e49c..55c96ee9 100644 --- a/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift +++ b/Codive/Features/Notification/Presentation/ViewModel/NotificationViewModel.swift @@ -41,19 +41,9 @@ final class NotificationViewModel: ObservableObject { try await useCase.markNotificationAsRead(notificationId: notificationId) if let index = unreadNotifications.firstIndex(where: { $0.notificationId == notificationId }) { - let readItem = unreadNotifications.remove(at: index) - - let updatedItem = NotificationEntity( - notificationId: readItem.notificationId, - notificationImageUrl: readItem.notificationImageUrl, - notificationContent: readItem.notificationContent, - redirectInfo: readItem.redirectInfo, - redirectType: readItem.redirectType, - readStatus: .read, - createdAt: readItem.createdAt - ) - - readNotifications.insert(updatedItem, at: 0) + var readItem = unreadNotifications.remove(at: index) + readItem.readStatus = .read + readNotifications.insert(readItem, at: 0) } } catch { readErrorMessage = "알림 읽음 처리에 실패했어요. 잠시 후 다시 시도해 주세요." From 14a9a3bcc12c95b163f34ff3903465361e61d3bf Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 13:05:41 +0900 Subject: [PATCH 15/21] =?UTF-8?q?[#45]=20SearchResultView=EC=97=90=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=EB=90=9C=20struct=EB=A5=BC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=EC=8B=9C=ED=82=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Presentation/View/HashtagView.swift | 52 +++++++++ .../View/SearchResultSegmentControl.swift | 63 +++++++++++ .../Presentation/View/SearchResultView.swift | 102 +----------------- 3 files changed, 116 insertions(+), 101 deletions(-) create mode 100644 Codive/Features/Search/Presentation/View/HashtagView.swift create mode 100644 Codive/Features/Search/Presentation/View/SearchResultSegmentControl.swift diff --git a/Codive/Features/Search/Presentation/View/HashtagView.swift b/Codive/Features/Search/Presentation/View/HashtagView.swift new file mode 100644 index 00000000..33457ab9 --- /dev/null +++ b/Codive/Features/Search/Presentation/View/HashtagView.swift @@ -0,0 +1,52 @@ +// +// HashtagView.swift +// Codive +// +// Created by 한금준 on 12/31/25. +// + +import SwiftUI + +// MARK: - Hashtag View +struct HashtagView: View { + @ObservedObject var viewModel: SearchResultViewModel + + // MARK: - Computed Properties + private var sortOptionsString: [String] { + viewModel.sortOptions.map { $0.displayName } + } + + // MARK: - Body + var body: some View { + VStack { + HStack { + Text("\(TextLiteral.Search.totalCount) \(viewModel.posts.count)\(TextLiteral.Search.countUnit)") + .font(Font.codive_body2_medium) + .foregroundStyle(Color.Codive.grayscale3) + Spacer() + + SortOption( + mainText: TextLiteral.Search.sortAll, + options: sortOptionsString, + selectedOption: $viewModel.currentSort + ) + .zIndex(10) + } + .padding(.top, 18) + .zIndex(10) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 11) { + ForEach(viewModel.posts) { post in + PostCard( + postImageUrl: post.postImageUrl, + profileImageUrl: post.profileImageUrl, + nickname: post.nickname + ) + } + } + .padding(.top, 18) + .zIndex(1) + } + .padding(.bottom, 20) + } +} diff --git a/Codive/Features/Search/Presentation/View/SearchResultSegmentControl.swift b/Codive/Features/Search/Presentation/View/SearchResultSegmentControl.swift new file mode 100644 index 00000000..7ecaef7c --- /dev/null +++ b/Codive/Features/Search/Presentation/View/SearchResultSegmentControl.swift @@ -0,0 +1,63 @@ +// +// SearchResultSegmentControl.swift +// Codive +// +// Created by 한금준 on 12/31/25. +// + +import SwiftUI + +// MARK: - Segment Type +enum SearchResultSegment { + case account + case hashtag +} + +// MARK: - Segment Control +struct SearchResultSegmentControl: View { + @Binding var selectedSegment: SearchResultSegment + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + segmentItem(title: TextLiteral.Search.account, segment: .account) + segmentItem(title: TextLiteral.Search.hashtag, segment: .hashtag) + } + + Rectangle() + .frame(height: 2) + .foregroundStyle(Color.Codive.grayscale6) + } + } + + @ViewBuilder + private func segmentItem(title: String, segment: SearchResultSegment) -> some View { + Button { + selectedSegment = segment + } label: { + VStack(spacing: 6) { + Text(title) + .font( + selectedSegment == segment + ? .codive_body1_medium + : .codive_body1_regular + ) + .foregroundStyle( + selectedSegment == segment + ? Color.Codive.grayscale1 + : Color.Codive.grayscale4 + ) + + Rectangle() + .frame(height: 2) + .foregroundStyle( + selectedSegment == segment + ? Color.Codive.point1 + : .clear + ) + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + } +} diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index c312dd2c..032f5af9 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -7,13 +7,6 @@ import SwiftUI -// MARK: - Segment Type -enum SearchResultSegment { - case account - case hashtag -} - -// MARK: - Search Result struct SearchResultView: View { // MARK: - Properties @StateObject private var viewModel: SearchResultViewModel @@ -63,7 +56,7 @@ struct SearchResultView: View { .padding(.top, 18) } else { // MARK: - Hashtag Result Grid - Hashtag(viewModel: viewModel) + HashtagView(viewModel: viewModel) .padding(.horizontal, 20) } } @@ -76,96 +69,3 @@ struct SearchResultView: View { } } } - -// MARK: - Hashtag View -struct Hashtag: View { - @ObservedObject var viewModel: SearchResultViewModel - - // MARK: - Computed Properties - private var sortOptionsString: [String] { - viewModel.sortOptions.map { $0.displayName } - } - - // MARK: - Body - var body: some View { - VStack { - HStack { - Text("\(TextLiteral.Search.totalCount) \(viewModel.posts.count)\(TextLiteral.Search.countUnit)") - .font(Font.codive_body2_medium) - .foregroundStyle(Color.Codive.grayscale3) - Spacer() - - SortOption( - mainText: TextLiteral.Search.sortAll, - options: sortOptionsString, - selectedOption: $viewModel.currentSort - ) - .zIndex(10) - } - .padding(.top, 18) - .zIndex(10) - - LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 11) { - ForEach(viewModel.posts) { post in - PostCard( - postImageUrl: post.postImageUrl, - profileImageUrl: post.profileImageUrl, - nickname: post.nickname - ) - } - } - .padding(.top, 18) - .zIndex(1) - } - .padding(.bottom, 20) - } -} - -// MARK: - Segment Control -struct SearchResultSegmentControl: View { - @Binding var selectedSegment: SearchResultSegment - - var body: some View { - VStack(spacing: 0) { - HStack(spacing: 0) { - segmentItem(title: TextLiteral.Search.account, segment: .account) - segmentItem(title: TextLiteral.Search.hashtag, segment: .hashtag) - } - - Rectangle() - .frame(height: 2) - .foregroundStyle(Color.Codive.grayscale6) - } - } - - @ViewBuilder - private func segmentItem(title: String, segment: SearchResultSegment) -> some View { - Button { - selectedSegment = segment - } label: { - VStack(spacing: 6) { - Text(title) - .font( - selectedSegment == segment - ? .codive_body1_medium - : .codive_body1_regular - ) - .foregroundStyle( - selectedSegment == segment - ? Color.Codive.grayscale1 - : Color.Codive.grayscale4 - ) - - Rectangle() - .frame(height: 2) - .foregroundStyle( - selectedSegment == segment - ? Color.Codive.point1 - : .clear - ) - } - .frame(maxWidth: .infinity) - } - .buttonStyle(.plain) - } -} From 9d95cc22d3f1fc4fcd47542efd69f3ec291c079d Mon Sep 17 00:00:00 2001 From: Funital Date: Wed, 31 Dec 2025 13:14:19 +0900 Subject: [PATCH 16/21] =?UTF-8?q?[#45]=20=EC=B5=9C=EA=B7=BC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=20tag=EB=A5=BC=20=EB=88=8C=EB=A0=80=EC=9D=84?= =?UTF-8?q?=EB=95=8C=20=EA=B2=80=EC=83=89=ED=95=98=EA=B8=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Search/Data/DataSources/SearchDataSource.swift | 4 ++-- .../Search/Presentation/View/SearchView.swift | 14 ++++++++++++-- .../Presentation/ViewModel/SearchViewModel.swift | 5 +++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Codive/Features/Search/Data/DataSources/SearchDataSource.swift b/Codive/Features/Search/Data/DataSources/SearchDataSource.swift index cc7c8cdc..b1f3c60e 100644 --- a/Codive/Features/Search/Data/DataSources/SearchDataSource.swift +++ b/Codive/Features/Search/Data/DataSources/SearchDataSource.swift @@ -17,9 +17,9 @@ final class SearchDataSource { func fetchRecentSearchTags() -> [SearchTagEntity] { return [ - SearchTagEntity(id: 1, text: "드뮤어룩"), + SearchTagEntity(id: 1, text: "겨울"), SearchTagEntity(id: 2, text: "한강룩"), - SearchTagEntity(id: 3, text: "여름 원피스"), + SearchTagEntity(id: 3, text: "한금준"), SearchTagEntity(id: 4, text: "패션이 좋아요") ] } diff --git a/Codive/Features/Search/Presentation/View/SearchView.swift b/Codive/Features/Search/Presentation/View/SearchView.swift index 9429dfbc..f3966cb0 100644 --- a/Codive/Features/Search/Presentation/View/SearchView.swift +++ b/Codive/Features/Search/Presentation/View/SearchView.swift @@ -65,9 +65,19 @@ struct SearchView: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(viewModel.recentSearchTags) { tag in - SearchTagView(text: tag.text) { - viewModel.deleteTag(tag: tag) + // 1. 태그 전체를 버튼화하여 클릭 액션 추가 + Button { + // 2. 검색창 텍스트 업데이트 + self.searchText = tag.text + // 3. ViewModel 검색 로직 실행 + viewModel.handleTagTap(tag: tag) + } label: { + SearchTagView(text: tag.text) { + // 삭제 버튼은 별도로 동작 (SearchTagView 내부 Button) + viewModel.deleteTag(tag: tag) + } } + .buttonStyle(PlainButtonStyle()) // 기본 버튼 스타일 제거 } } } diff --git a/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift b/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift index 077d558d..c2012c19 100644 --- a/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift +++ b/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift @@ -59,6 +59,11 @@ final class SearchViewModel: ObservableObject { self.recentSearchTags = [] } + func handleTagTap(tag: SearchTagEntity) { + // 검색 실행 로직 호출 + executeSearch(query: tag.text) + } + // MARK: - Navigation func handleBackTap() { navigationRouter.navigateBack() From 4bb9e824dded126a856b25dbed53907006017d93 Mon Sep 17 00:00:00 2001 From: Funital Date: Thu, 1 Jan 2026 02:59:38 +0900 Subject: [PATCH 17/21] =?UTF-8?q?[#45]=20=ED=85=8C=EB=91=90=EB=A6=AC=20?= =?UTF-8?q?=EC=97=AC=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- Codive/Features/Search/Presentation/View/SearchView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Codive/Features/Search/Presentation/View/SearchView.swift b/Codive/Features/Search/Presentation/View/SearchView.swift index f3966cb0..5c5f48a9 100644 --- a/Codive/Features/Search/Presentation/View/SearchView.swift +++ b/Codive/Features/Search/Presentation/View/SearchView.swift @@ -76,6 +76,8 @@ struct SearchView: View { // 삭제 버튼은 별도로 동작 (SearchTagView 내부 Button) viewModel.deleteTag(tag: tag) } + .padding(.vertical, 2) + .padding(.horizontal, 2) } .buttonStyle(PlainButtonStyle()) // 기본 버튼 스타일 제거 } From 5129b676a8d1644e3421379ee430687cc427621f Mon Sep 17 00:00:00 2001 From: Funital Date: Thu, 1 Jan 2026 03:05:48 +0900 Subject: [PATCH 18/21] =?UTF-8?q?[#45]=20=ED=82=A4=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=82=B4=EB=A0=A4=EA=B0=80=EA=B8=B0=20=EC=A0=9C=EC=8A=A4?= =?UTF-8?q?=EC=B2=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- Codive/Features/Search/Presentation/View/SearchView.swift | 6 +++++- .../DesignSystem/UIHelpers/View+CustomCornerRadius.swift | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Codive/Features/Search/Presentation/View/SearchView.swift b/Codive/Features/Search/Presentation/View/SearchView.swift index 5c5f48a9..3c4e6138 100644 --- a/Codive/Features/Search/Presentation/View/SearchView.swift +++ b/Codive/Features/Search/Presentation/View/SearchView.swift @@ -63,7 +63,7 @@ struct SearchView: View { .padding(.top, 8) } else { ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { + HStack(spacing: 10) { ForEach(viewModel.recentSearchTags) { tag in // 1. 태그 전체를 버튼화하여 클릭 액션 추가 Button { @@ -119,6 +119,10 @@ struct SearchView: View { .onAppear { viewModel.loadData() } + .onTapGesture { + // 화면의 빈 곳을 터치하면 키보드를 내림 + hideKeyboard() + } // MARK: - Alert .alert( TextLiteral.Search.alertTitle, diff --git a/Codive/Shared/DesignSystem/UIHelpers/View+CustomCornerRadius.swift b/Codive/Shared/DesignSystem/UIHelpers/View+CustomCornerRadius.swift index 326daccb..42473d60 100644 --- a/Codive/Shared/DesignSystem/UIHelpers/View+CustomCornerRadius.swift +++ b/Codive/Shared/DesignSystem/UIHelpers/View+CustomCornerRadius.swift @@ -11,4 +11,8 @@ extension View { func customCornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { clipShape(RoundedCorner(radius: radius, corners: corners)) } + + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } } From 2f01b88d883729c555e9fae8c8be41d95484473f Mon Sep 17 00:00:00 2001 From: Funital Date: Thu, 1 Jan 2026 03:24:45 +0900 Subject: [PATCH 19/21] =?UTF-8?q?[#45]=20=EA=B2=80=EC=83=89=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=8F=99=EC=9E=91=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EB=A1=9C=EC=A7=81=20=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../View/myCloth/MyClosetView.swift | 12 ++++++--- .../Presentation/View/SearchResultView.swift | 17 +++++++++++-- .../Search/Presentation/View/SearchView.swift | 21 +++++++++++----- .../ClothSheet/CustomProductBottomSheet.swift | 4 ++- .../DesignSystem/Inputs/CustomSearchBar.swift | 25 +++++++++++++------ 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/Codive/Features/Closet/Presentation/View/myCloth/MyClosetView.swift b/Codive/Features/Closet/Presentation/View/myCloth/MyClosetView.swift index 7d4d822b..7509c362 100644 --- a/Codive/Features/Closet/Presentation/View/myCloth/MyClosetView.swift +++ b/Codive/Features/Closet/Presentation/View/myCloth/MyClosetView.swift @@ -44,10 +44,14 @@ struct MyClosetView: View { } } : .none ) - - CustomSearchBar(text: $viewModel.searchText, type: .normal) - .padding(.horizontal, 20) - .padding(.vertical, 10) + CustomSearchBar( + text: $viewModel.searchText, + type: .normal + ) { + hideKeyboard() + } + .padding(.horizontal, 20) + .padding(.vertical, 10) mainCategoryTab diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index 032f5af9..3a45ddd8 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -30,7 +30,10 @@ struct SearchResultView: View { type: .withBackButton { viewModel.handleBackTap() } - ) + ) { + viewModel.executeNewSearch(query: viewModel.searchBarText) + hideKeyboard() + } .padding(.horizontal, 20) .zIndex(1) .onSubmit { @@ -60,9 +63,19 @@ struct SearchResultView: View { .padding(.horizontal, 20) } } + .contentShape(Rectangle()) + .onTapGesture { + hideKeyboard() + } } .navigationBarHidden(true) - .background(Color.white.ignoresSafeArea(.all)) + .background( + Color.white + .ignoresSafeArea(.all) + .onTapGesture { // 3. 배경 터치 시 키보드 내림 + hideKeyboard() + } + ) // MARK: - Data Loading Trigger .onAppear { viewModel.loadInitialData() diff --git a/Codive/Features/Search/Presentation/View/SearchView.swift b/Codive/Features/Search/Presentation/View/SearchView.swift index 3c4e6138..fa4151fa 100644 --- a/Codive/Features/Search/Presentation/View/SearchView.swift +++ b/Codive/Features/Search/Presentation/View/SearchView.swift @@ -25,7 +25,10 @@ struct SearchView: View { type: .withBackButton { viewModel.handleBackTap() } - ) + ) { + viewModel.executeSearch(query: searchText) + hideKeyboard() + } .onSubmit { viewModel.executeSearch(query: searchText) } @@ -110,19 +113,25 @@ struct SearchView: View { }.padding(.top, 8) } .padding(.bottom, 20) + .contentShape(Rectangle()) + .onTapGesture { + hideKeyboard() + } } } .navigationBarHidden(true) - .background(Color.white.ignoresSafeArea(.all)) + .background( + Color.white + .ignoresSafeArea(.all) + .onTapGesture { // 3. 배경 터치 시 키보드 내림 + hideKeyboard() + } + ) .padding(.horizontal, 20) // MARK: - Data Loading Trigger .onAppear { viewModel.loadData() } - .onTapGesture { - // 화면의 빈 곳을 터치하면 키보드를 내림 - hideKeyboard() - } // MARK: - Alert .alert( TextLiteral.Search.alertTitle, diff --git a/Codive/Shared/DesignSystem/ClothSheet/CustomProductBottomSheet.swift b/Codive/Shared/DesignSystem/ClothSheet/CustomProductBottomSheet.swift index d2bb188d..d160442f 100644 --- a/Codive/Shared/DesignSystem/ClothSheet/CustomProductBottomSheet.swift +++ b/Codive/Shared/DesignSystem/ClothSheet/CustomProductBottomSheet.swift @@ -30,7 +30,9 @@ struct CustomProductBottomSheet: View { CustomSearchBar( text: $searchText, type: .normal - ) + ) { + hideKeyboard() + } .padding(.horizontal, 20) // 카테고리 태그 diff --git a/Codive/Shared/DesignSystem/Inputs/CustomSearchBar.swift b/Codive/Shared/DesignSystem/Inputs/CustomSearchBar.swift index aa1105bc..2eddf0b0 100644 --- a/Codive/Shared/DesignSystem/Inputs/CustomSearchBar.swift +++ b/Codive/Shared/DesignSystem/Inputs/CustomSearchBar.swift @@ -15,6 +15,7 @@ enum SearchBarType { struct CustomSearchBar: View { @Binding var text: String var type: SearchBarType = .normal + var onSearch: () -> Void var body: some View { HStack { @@ -42,8 +43,11 @@ struct CustomSearchBar: View { .foregroundStyle(Color.Codive.grayscale1) } - Image(systemName: "magnifyingglass") - .foregroundStyle(Color.Codive.main1) + Button(action: onSearch) { + Image(systemName: "magnifyingglass") + .foregroundStyle(Color.Codive.main1) + .padding(4) + } } .padding(.horizontal, 12) .padding(.vertical, 12) @@ -70,18 +74,25 @@ struct StatefulPreviewWrapper: View { #Preview { VStack(spacing: 16) { // 일반 타입 - StatefulPreviewWrapper("") { - CustomSearchBar(text: $0, type: .normal) + StatefulPreviewWrapper("") { $text in + CustomSearchBar( + text: $text, + type: .normal + ) { + print("검색어: \(text)") + } } // 뒤로가기 버튼 타입 - StatefulPreviewWrapper("") { + StatefulPreviewWrapper("") { $text in CustomSearchBar( - text: $0, + text: $text, type: .withBackButton { print("뒤로가기 버튼 눌림") } - ) + ) { + print("검색 실행: \(text)") + } } } .padding(.horizontal, 20) From 1a71b0a491575a6f168bd56ad92231bdcf547ba2 Mon Sep 17 00:00:00 2001 From: Funital Date: Thu, 1 Jan 2026 14:05:28 +0900 Subject: [PATCH 20/21] =?UTF-8?q?[#45]=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../heart_off_black.imageset/Contents.json | 2 +- .../heart_off_black.imageset/heart_off_black.pdf | Bin .../Icon_folder/history.imageset/Contents.json | 2 +- .../Icon_folder/history.imageset/history.png | Bin .../Icon_folder/weather.imageset/Contents.json | 2 +- .../Icon_folder/weather.imageset/weather.png | Bin 6 files changed, 3 insertions(+), 3 deletions(-) rename "Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/\355\225\230\355\212\270.pdf" => Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/heart_off_black.pdf (100%) rename "Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" => Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/history.png (100%) rename "Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" => Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/weather.png (100%) diff --git a/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/Contents.json b/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/Contents.json index af4ef580..028b337e 100644 --- a/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/Contents.json +++ b/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "하트.pdf", + "filename" : "heart_off_black.pdf", "idiom" : "universal" } ], diff --git "a/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/\355\225\230\355\212\270.pdf" b/Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/heart_off_black.pdf similarity index 100% rename from "Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/\355\225\230\355\212\270.pdf" rename to Codive/Resources/Icons.xcassets/Icon_folder/heart_off_black.imageset/heart_off_black.pdf diff --git a/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json index 060462d4..ccfd3436 100644 --- a/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json +++ b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "아이콘-2.png", + "filename" : "history.png", "idiom" : "universal" } ], diff --git "a/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" b/Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/history.png similarity index 100% rename from "Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/\354\225\204\354\235\264\354\275\230-2.png" rename to Codive/Resources/Icons.xcassets/Icon_folder/history.imageset/history.png diff --git a/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json index 7f1dafbc..fc647c71 100644 --- a/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json +++ b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "아이콘.png", + "filename" : "weather.png", "idiom" : "universal" } ], diff --git "a/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" b/Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/weather.png similarity index 100% rename from "Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/\354\225\204\354\235\264\354\275\230.png" rename to Codive/Resources/Icons.xcassets/Icon_folder/weather.imageset/weather.png From 0805f34915c490b04be69ddac846890ffb56ba87 Mon Sep 17 00:00:00 2001 From: Funital Date: Thu, 1 Jan 2026 16:29:18 +0900 Subject: [PATCH 21/21] =?UTF-8?q?[#45]=20=EA=B2=80=EC=83=89=20=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=8A=A4=ED=83=9D=20=EC=8C=93=EC=9D=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #45 --- .../Search/Presentation/View/SearchResultView.swift | 3 ++- .../Search/Presentation/View/SearchView.swift | 8 ++------ .../ViewModel/SearchResultViewModel.swift | 11 ++++------- .../Presentation/ViewModel/SearchViewModel.swift | 3 +-- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Codive/Features/Search/Presentation/View/SearchResultView.swift b/Codive/Features/Search/Presentation/View/SearchResultView.swift index 3a45ddd8..ed404b59 100644 --- a/Codive/Features/Search/Presentation/View/SearchResultView.swift +++ b/Codive/Features/Search/Presentation/View/SearchResultView.swift @@ -38,6 +38,7 @@ struct SearchResultView: View { .zIndex(1) .onSubmit { viewModel.executeNewSearch(query: viewModel.searchBarText) + hideKeyboard() } SearchResultSegmentControl(selectedSegment: $selectedSegment) @@ -72,7 +73,7 @@ struct SearchResultView: View { .background( Color.white .ignoresSafeArea(.all) - .onTapGesture { // 3. 배경 터치 시 키보드 내림 + .onTapGesture { hideKeyboard() } ) diff --git a/Codive/Features/Search/Presentation/View/SearchView.swift b/Codive/Features/Search/Presentation/View/SearchView.swift index fa4151fa..b24113e4 100644 --- a/Codive/Features/Search/Presentation/View/SearchView.swift +++ b/Codive/Features/Search/Presentation/View/SearchView.swift @@ -68,21 +68,17 @@ struct SearchView: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(viewModel.recentSearchTags) { tag in - // 1. 태그 전체를 버튼화하여 클릭 액션 추가 Button { - // 2. 검색창 텍스트 업데이트 self.searchText = tag.text - // 3. ViewModel 검색 로직 실행 viewModel.handleTagTap(tag: tag) } label: { SearchTagView(text: tag.text) { - // 삭제 버튼은 별도로 동작 (SearchTagView 내부 Button) viewModel.deleteTag(tag: tag) } .padding(.vertical, 2) .padding(.horizontal, 2) } - .buttonStyle(PlainButtonStyle()) // 기본 버튼 스타일 제거 + .buttonStyle(PlainButtonStyle()) } } } @@ -123,7 +119,7 @@ struct SearchView: View { .background( Color.white .ignoresSafeArea(.all) - .onTapGesture { // 3. 배경 터치 시 키보드 내림 + .onTapGesture { hideKeyboard() } ) diff --git a/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift b/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift index c82a1a2d..0c574931 100644 --- a/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift +++ b/Codive/Features/Search/Presentation/ViewModel/SearchResultViewModel.swift @@ -18,7 +18,7 @@ final class SearchResultViewModel: ObservableObject { private var initialQuery: String @Published var posts: [PostEntity] = [] - @Published var users: [SimpleUser] = [] // 🔸 계정 탭용 + @Published var users: [SimpleUser] = [] @Published var currentSort: String = "전체" @Published var searchBarText: String @@ -66,8 +66,7 @@ final class SearchResultViewModel: ObservableObject { } // MARK: - Public Methods - - /// 게시글 + 유저를 한 번에 초기 로딩 + func loadInitialData() { loadPosts() loadUsers() @@ -94,13 +93,11 @@ final class SearchResultViewModel: ObservableObject { self.initialQuery = trimmedQuery self.currentSort = "전체" - - // 🔸 새 검색 시 게시글 + 유저 둘 다 갱신 + loadPosts() loadUsers() - navigationRouter.navigate(to: .searchResult(query: trimmedQuery)) - print("새로운 검색 실행: \(trimmedQuery)") + print("현재 페이지에서 검색 결과 갱신: \(trimmedQuery)") } // MARK: - Navigation diff --git a/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift b/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift index c2012c19..5b06c72b 100644 --- a/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift +++ b/Codive/Features/Search/Presentation/ViewModel/SearchViewModel.swift @@ -38,7 +38,7 @@ final class SearchViewModel: ObservableObject { print("검색어를 입력해 주세요.") return } - // query를 전달하도록 수정 + navigationRouter.navigate(to: .searchResult(query: trimmedQuery)) print("검색 실행: \(trimmedQuery). SearchResultView로 이동 필요.") } @@ -60,7 +60,6 @@ final class SearchViewModel: ObservableObject { } func handleTagTap(tag: SearchTagEntity) { - // 검색 실행 로직 호출 executeSearch(query: tag.text) }