From cbd38858a5def6bf01ee2969b4498c49c767f57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Thu, 16 Oct 2025 11:37:25 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[FEAT]=20=EC=9E=AC=EA=B3=A0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/feature/auth/ui/LoginView.swift | 4 +- .../feature/inventory/data/InventoryApi.swift | 66 ++++++ .../data/InventoryRepositoryImpl.swift | 28 +++ .../domain/InventoryRepositoryProtocol.swift | 18 ++ .../inventory/ui/InventorySearchView.swift | 223 ++++++++++++++++++ .../feature/inventory/ui/InventoryView.swift | 2 +- .../viewmodel/InventoryViewModel.swift | 104 ++++++++ 7 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift create mode 100644 StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift create mode 100644 StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift create mode 100644 StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift create mode 100644 StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index 23da13f..06d745a 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -121,8 +121,8 @@ struct LoginView: View { // MARK: - 유효성 검사 함수 private func isValidForm() -> Bool { emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" - pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" - return emailError == nil && pwError == nil + //pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" + return emailError == nil //&& pwError == nil } } diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift new file mode 100644 index 0000000..1a2720c --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift @@ -0,0 +1,66 @@ +// +// InventoryApi.swift +// StockMate +// +// Created by Admin on 10/15/25. +// + + +import Foundation +import Alamofire + +struct InventoryResponse: Decodable { + let status: Int + let success: Bool + let message: String + let data: InventoryPageData? +} + +struct InventoryPageData: Decodable { + let content: [InventoryItem] + let page: Int + let size: Int + let totalElements: Int + let totalPages: Int +} + +struct InventoryItem: Decodable, Identifiable { + let id: Int + let name: String + let price: Int + let image: String + let trim: String + let model: String + let category: Int + let korName: String + let engName: String + let categoryName: String + let stock: Int // 본사 재고 + let amount: Int // 지점별 재고 + let limitAmount: Int // 지점별 최소 수량 + let isLack: Bool +} + +enum InventoryApi { + static func getInventoryList( + page: Int, + size: Int, + categoryNames: [String], + trims: [String], + models: [String] + ) -> DataRequest { + var url = ApiClient.baseURL + "api/v1/store/search?page=\(page)&size=\(size)" + + for c in categoryNames { + url += "&categoryName=\(c.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + for t in trims { + url += "&trim=\(t.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + for m in models { + url += "&model=\(m.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + + return ApiClient.shared.request(url, method: .get) + } +} diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift new file mode 100644 index 0000000..7436a94 --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift @@ -0,0 +1,28 @@ +// +// InventoryRepositoryImpl.swift +// StockMate +// +// Created by Admin on 10/15/25. +// + +import Foundation +import Alamofire + +final class InventoryRepositoryImpl: InventoryRepositoryProtocol { + func getInventoryList( + page: Int, + size: Int, + categoryNames: [String], + trims: [String], + models: [String] + ) async -> AppResult> { + let dataReq = InventoryApi.getInventoryList( + page: page, + size: size, + categoryNames: categoryNames, + trims: trims, + models: models + ) + return await safeApi(dataReq, decodeTo: ApiResponse.self) + } +} diff --git a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift new file mode 100644 index 0000000..f3c595e --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift @@ -0,0 +1,18 @@ +// +// InventoryRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 10/15/25. +// + +import Foundation + +protocol InventoryRepositoryProtocol { + func getInventoryList( + page: Int, + size: Int, + categoryNames: [String], + trims: [String], + models: [String] + ) async -> AppResult> +} diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift new file mode 100644 index 0000000..bed009c --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -0,0 +1,223 @@ +// +// InventorySearchView.swift +// StockMate +// +// Created by Admin on 10/15/25. +// + + +import SwiftUI + +struct InventorySearchView: View { + @StateObject private var inventoryViewModel = InventoryViewModel() + @State private var searchText = "" + + private let categories = ["전기/램프", "엔진/미션", "하체/바디", "내장/외장", "기타소모품"] + private let trims = ["준중형/소형", "중형", "대형", "SUV", "화물/트럭/승합", "수소/전기"] + + private let trimToModels: [String: [String]] = [ + "준중형/소형": ["아반떼MD", "아반떼AD", "아반떼CN7", "I30", "엑센트", "아이오닉", "벨로스터", "캐스퍼"], + "중형": ["NF소나타", "YF소나타", "LF소나타", "DN8소나타", "그랜저TG", "그랜저HG", "그랜저IG", "그랜저GN7", "I40"], + "대형": ["제네시스BH", "에쿠스"], + "SUV": ["베뉴", "코나OS", "코나SX2", "투싼IX", "투싼TL", "투싼NX4", "싼타페CM", "싼타페DM", "싼타페TM", "싼타페MX5", "맥스크루즈", "베라크루즈", "팰리세이드LX2", "팰리세이드LX3"], + "화물/트럭/승합": ["스타렉스", "그랜드스타렉스", "스타리아", "포터2", "쏠라티", "마이티", "메가트럭", "카운티"], + "수소/전기자동차": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] + ] + + private var filteredModels: [String] { + if inventoryViewModel.selectedTrims.isEmpty { + return trimToModels.values.flatMap { $0 } + } else { + return inventoryViewModel.selectedTrims.flatMap { trimToModels[$0] ?? [] } + } + } + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + // 🔍 검색창 + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + TextField("부품을 검색하세요.", text: $searchText) + .textFieldStyle(PlainTextFieldStyle()) + } + .padding() + .background(Color(.white)) + .cornerRadius(9999) + .padding(.horizontal) + .padding(.vertical) + + // 🔽 필터 버튼 + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + FilterMenu( + title: "카테고리", + items: categories, + selected: { inventoryViewModel.selectedCategories.contains($0) }, + onTap: { inventoryViewModel.toggleCategory($0) } + ) + FilterMenu( + title: "분류", + items: trims, + selected: { inventoryViewModel.selectedTrims.contains($0) }, + onTap: { inventoryViewModel.toggleTrim($0) } + ) + FilterMenu( + title: "모델", + items: filteredModels, + selected: { inventoryViewModel.selectedModels.contains($0) }, + onTap: { inventoryViewModel.toggleModel($0) } + ) + } + .padding(.horizontal) + .padding(.top, 2) + .padding(.bottom, 4) + } + + // 📋 재고 리스트 + ScrollView { + LazyVStack(spacing: 10) { + ForEach(inventoryViewModel.inventoryItems) { item in + InventoryCard(item: item) + .padding(.horizontal) + .onAppear { + if item.id == inventoryViewModel.inventoryItems.last?.id, + inventoryViewModel.hasMore { + Task { + await inventoryViewModel.loadInventoryList() + } + } + } + } + + if inventoryViewModel.isLoading && inventoryViewModel.hasMore { + ProgressView() + .padding(.vertical) + } + } + .padding(.top, 8) + } + } + .background(Color.Light) + .navigationTitle("재고 조회") + .task { + await inventoryViewModel.loadInventoryList(reset: true) + } + } + } +} + +// ✅ 재고 카드 (UI 스타일 적용) +struct InventoryCard: View { + let item: InventoryItem + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(item.categoryName) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + Divider() + .frame(height: 0.2) + .background(Color.textGray2) + + HStack(alignment: .center, spacing: 12) { + AsyncImage(url: URL(string: item.image)) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(item.korName) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + Spacer() + } + .padding(.top, 2) + + Text("\(item.trim)/\(item.model)") + .font(.system(size: 13, weight: .regular)) + .foregroundColor(.gray) + + + + } + .frame(height: 60, alignment: .top) + + VStack (alignment: .center, spacing: 6) { + if item.isLack { + Text("수량 부족") + .font(.system(size: 13, weight: .regular)) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(Color.DangerBg) + .foregroundColor(.Danger) + .cornerRadius(12) + } else { + Text("수량 여유") + .font(.system(size: 13, weight: .regular)) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(Color.StatusGreenBg) + .foregroundColor(.StatusGreen) + .cornerRadius(12) + } + + VStack(alignment: .leading){ + Text("현재수량: \(item.amount)개") + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.textGray1) + + Text("최소수량: \(item.limitAmount)개") + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.textGray1) + } + } + } + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} + +// ✅ 필터 버튼 공용 컴포넌트 +struct FilterMenu: View { + let title: String + let items: [String] + let selected: (String) -> Bool + let onTap: (String) -> Void + + var body: some View { + Menu { + ForEach(items, id: \.self) { item in + Button { + onTap(item) + } label: { + HStack { + Text(item) + if selected(item) { + Spacer() + Image(systemName: "checkmark") + } + } + } + } + } label: { + Text(title) + .font(.subheadline) + .foregroundColor(.black) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color(.systemGray6)) + .cornerRadius(8) + } + } +} diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index 96756f0..2ae4bf1 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -56,7 +56,7 @@ struct InventoryView: View { struct GridMenuView: View { let menuItems = [ - ("재고조회", Color.InvIncoming, Color.InvIncomingBg, AnyView(IncomingScanView())), + ("재고조회", Color.InvIncoming, Color.InvIncomingBg, AnyView(InventorySearchView())), ("입출고 히스토리", Color.InvUse, Color.InvUseBg, AnyView(IncomingScanView())), ("입고처리", Color.Transfer, Color.TransferBg, AnyView(IncomingScanView())), ("사용처리", Color.InvStock, Color.InvStockBg, AnyView(IncomingScanView())), diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift new file mode 100644 index 0000000..6975c61 --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -0,0 +1,104 @@ +// +// InventoryViewModel.swift +// StockMate +// +// Created by Admin on 10/15/25. +// + +import SwiftUI + +@MainActor +final class InventoryViewModel: ObservableObject { + @Published var inventoryItems: [InventoryItem] = [] + @Published var message: String = "" + @Published var shouldGoToLogin: Bool = false + + @Published var selectedCategories: [String] = [] + @Published var selectedTrims: [String] = [] + @Published var selectedModels: [String] = [] + + @Published var currentPage = 0 + @Published var isLoading = false + @Published var hasMore = true + + private let repo: InventoryRepositoryProtocol + + init(repo: InventoryRepositoryProtocol = InventoryRepositoryImpl()) { + self.repo = repo + } + + func loadInventoryList( + reset: Bool = false, + size: Int = 20 + ) async { + guard !isLoading else { return } + isLoading = true + + if reset { + currentPage = 0 + inventoryItems.removeAll() + hasMore = true + } + + let result = await repo.getInventoryList( + page: currentPage, + size: size, + categoryNames: selectedCategories, + trims: selectedTrims, + models: selectedModels + ) + + switch result { + case .success(let apiResp): + if let data = apiResp.data { + if reset { + inventoryItems = data.content + } else { + inventoryItems.append(contentsOf: data.content) + } + hasMore = currentPage + 1 < data.totalPages + currentPage += 1 + } else { + message = apiResp.message + } + case .failure(let err): + message = err.message + if err.code == 401 || err.code == 403 { + shouldGoToLogin = true + } + } + isLoading = false + } + + func resetAndLoad() async { + await loadInventoryList(reset: true) + } + + // MARK: - Filter toggle + func toggleCategory(_ name: String) { + if selectedCategories.contains(name) { + selectedCategories.removeAll { $0 == name } + } else { + selectedCategories.append(name) + } + Task { await resetAndLoad() } + } + + func toggleTrim(_ trim: String) { + if selectedTrims.contains(trim) { + selectedTrims.removeAll { $0 == trim } + } else { + selectedTrims.append(trim) + } + Task { await resetAndLoad() } + } + + func toggleModel(_ model: String) { + if selectedModels.contains(model) { + selectedModels.removeAll { $0 == model } + } else { + selectedModels.append(model) + } + Task { await resetAndLoad() } + } +} From 8392a162596471bae908b5e836011a1253a171ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Thu, 16 Oct 2025 20:25:07 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[FEAT]=20=EC=9E=AC=EA=B3=A0=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=83=AD=20=EB=B6=80=EC=A1=B1=EC=9E=AC=EA=B3=A0=20?= =?UTF-8?q?API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/components/InventoryCardView.swift | 91 ++++++ .../components/InventoryListSection.swift | 18 ++ .../feature/inventory/data/InventoryApi.swift | 9 + .../data/InventoryRepositoryImpl.swift | 9 + .../domain/InventoryRepositoryProtocol.swift | 6 + .../inventory/ui/InventorySearchView.swift | 14 +- .../feature/inventory/ui/InventoryView.swift | 271 +++++++++--------- .../viewmodel/InventoryViewModel.swift | 65 +++++ 8 files changed, 350 insertions(+), 133 deletions(-) create mode 100644 StockMate/StockMate/app/core/components/InventoryCardView.swift create mode 100644 StockMate/StockMate/app/core/components/InventoryListSection.swift diff --git a/StockMate/StockMate/app/core/components/InventoryCardView.swift b/StockMate/StockMate/app/core/components/InventoryCardView.swift new file mode 100644 index 0000000..7113417 --- /dev/null +++ b/StockMate/StockMate/app/core/components/InventoryCardView.swift @@ -0,0 +1,91 @@ +// +// InventoryCardView.swift +// StockMate +// +// Created by Admin on 10/16/25. +// + +import SwiftUI + + +struct InventoryCardView: View { + let item: InventoryItem + var showStatus: Bool = true // 필요 시 상태 표시 숨기기 옵션 + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + // 상단 카테고리 + Text(item.categoryName) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + Divider() + .frame(height: 0.2) + .background(Color.textGray2) + + HStack(alignment: .center, spacing: 12) { + // 제품 이미지 + AsyncImage(url: URL(string: item.image)) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + // 제품명, 트림/모델 + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(item.korName) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + Spacer() + } + .padding(.top, 2) + + Text("\(item.trim) / \(item.model)") + .font(.system(size: 13, weight: .regular)) + .foregroundColor(.gray) + } + .frame(height: 60, alignment: .top) + + // 상태 표시 (옵션) + if showStatus { + VStack(alignment: .center, spacing: 6) { + if item.isLack { + Text("수량 부족") + .font(.system(size: 13, weight: .regular)) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(Color.DangerBg) + .foregroundColor(.Danger) + .cornerRadius(12) + } else { + Text("수량 여유") + .font(.system(size: 13, weight: .regular)) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(Color.StatusGreenBg) + .foregroundColor(.StatusGreen) + .cornerRadius(12) + } + + VStack(alignment: .leading) { + Text("현재수량: \(item.amount)개") + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.textGray1) + Text("최소수량: \(item.limitAmount)개") + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.textGray1) + } + } + } + } + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} diff --git a/StockMate/StockMate/app/core/components/InventoryListSection.swift b/StockMate/StockMate/app/core/components/InventoryListSection.swift new file mode 100644 index 0000000..c8d6c19 --- /dev/null +++ b/StockMate/StockMate/app/core/components/InventoryListSection.swift @@ -0,0 +1,18 @@ +// +// InventoryListSection.swift +// StockMate +// +// Created by Admin on 10/16/25. +// + +import SwiftUI + +struct InventoryListSection: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + InventoryListSection() +} diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift index 1a2720c..2d250a9 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift @@ -63,4 +63,13 @@ enum InventoryApi { return ApiClient.shared.request(url, method: .get) } + + // ✅ 부족 재고 조회 API 추가 + static func getUnderLimitList(categoryName: String? = nil, page: Int = 0, size: Int = 10) -> DataRequest { + var url = ApiClient.baseURL + "api/v1/store/under-limit?page=\(page)&size=\(size)" + if let categoryName = categoryName, !categoryName.isEmpty { + url += "&categoryName=\(categoryName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + return ApiClient.shared.request(url, method: .get) + } } diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift index 7436a94..cb66353 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift @@ -25,4 +25,13 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { ) return await safeApi(dataReq, decodeTo: ApiResponse.self) } + // 부족 재고 리스트 호출 + func getUnderLimitList( + categoryName: String?, + page: Int, + size: Int + ) async -> AppResult> { + let dataReq = InventoryApi.getUnderLimitList(categoryName: categoryName, page: page, size: size) + return await safeApi(dataReq, decodeTo: ApiResponse.self) + } } diff --git a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift index f3c595e..eb7a550 100644 --- a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift @@ -15,4 +15,10 @@ protocol InventoryRepositoryProtocol { trims: [String], models: [String] ) async -> AppResult> + + func getUnderLimitList( + categoryName: String?, + page: Int, + size: Int + ) async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index bed009c..0455f6f 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -79,7 +79,7 @@ struct InventorySearchView: View { ScrollView { LazyVStack(spacing: 10) { ForEach(inventoryViewModel.inventoryItems) { item in - InventoryCard(item: item) + InventoryCardView(item: item) .padding(.horizontal) .onAppear { if item.id == inventoryViewModel.inventoryItems.last?.id, @@ -89,6 +89,18 @@ struct InventorySearchView: View { } } } + + +// InventoryCard(item: item) +// .padding(.horizontal) +// .onAppear { +// if item.id == inventoryViewModel.inventoryItems.last?.id, +// inventoryViewModel.hasMore { +// Task { +// await inventoryViewModel.loadInventoryList() +// } +// } +// } } if inventoryViewModel.isLoading && inventoryViewModel.hasMore { diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index 2ae4bf1..58ab734 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -6,165 +6,172 @@ // import SwiftUI - struct InventoryView: View { + @StateObject private var inventoryViewModel = InventoryViewModel() + @State private var showScrollToTopButton = false + @Namespace private var topID + var body: some View { NavigationStack { - VStack(alignment: .leading, spacing: 13) { - Text("재고 관리") - .font(.title2) - .bold() - .padding(.top, 13) - .frame(maxWidth: .infinity, alignment: .center) - - // 4개 버튼 영역 - GridMenuView() - - // 섹션 타이틀 - Text("얼마 남지 않았어요!") - .font(.system(size: 22, weight: .semibold)) - .foregroundColor(.black) - .padding(.horizontal, 25) - .padding(.top) - - // 공통 컴포넌트 리스트 - ScrollView { - VStack(spacing: 12) { - // 예시 데이터 - let dummyData: [(String, String, Int, Int)] = [ - ("브레이크", "현대 아이오닉5", 5, 10), - ("엔진오일", "기아 EV6", 10, 12), - ("에어필터", "현대 코나", 8, 10), - ] + ScrollViewReader { proxy in + ZStack { + ScrollView { + VStack(spacing: 0) { + // ✅ 스크롤 감지용 (맨 위) + GeometryReader { geo in + Color.clear + .onChange(of: geo.frame(in: .global).minY) { newValue in + // 👇 스크롤 시 값이 변함 + print("📏 Scroll offsetY:", newValue) // 테스트용, 화면 안정화 후 제거 + withAnimation(.easeInOut(duration: 0.25)) { + showScrollToTopButton = newValue < -150 + } + } + } + .frame(height: 0) + .id(topID) + + // 타이틀 + Text("재고 관리") + .font(.title2) + .bold() + .padding(.top, 13) + .padding(.leading, 25) + .frame(maxWidth: .infinity, alignment: .leading) + + GridMenuView() + + Text("얼마 남지 않았어요!") + .font(.system(size: 22, weight: .semibold)) + .foregroundColor(.black) + .padding(.horizontal, 25) + .padding(.top) + .frame(maxWidth: .infinity, alignment: .leading) + + LazyVStack(spacing: 12) { + ForEach(inventoryViewModel.underLimitItems) { item in + InventoryCardView(item: item) + .padding(.horizontal) + .onAppear { + if item.id == inventoryViewModel.underLimitItems.last?.id { + Task { + await inventoryViewModel.loadUnderLimitList() + } + } + } + } + + if inventoryViewModel.isLoading { + ProgressView() + .padding(.vertical, 20) + } + } + .padding(.top, 8) + .padding(.bottom, 80) + } + } - ForEach(dummyData, id: \.0) { item in - StockShortageCard( - partName: item.0, - carModel: item.1, - currentCount: item.2, - minCount: item.3 - ) + // 오른쪽 하단 플로팅 버튼 + if showScrollToTopButton { + VStack { + Spacer() + HStack { + Spacer() + Button { + withAnimation(.easeInOut) { + proxy.scrollTo(topID, anchor: .top) + } + } label: { + ZStack { + Circle() + .fill(Color.Secondary) // ✅ 배경색 + .frame(width: 50, height: 50) + Image(systemName: "arrow.up") + .font( + .system(size: 24, weight: .bold) + ) + .foregroundColor(.white) // ✅ 화살표 색 + } + } + .padding(.trailing, 20) + .padding(.bottom, 20) + } } + .transition(.opacity) + .animation(.easeInOut(duration: 0.25), value: showScrollToTopButton) } - .padding(.horizontal) + + + } + .background(Color.Light) + .task { + await inventoryViewModel.loadUnderLimitList(reset: true) } } - .background(Color.Light) } } } struct GridMenuView: View { let menuItems = [ - ("재고조회", Color.InvIncoming, Color.InvIncomingBg, AnyView(InventorySearchView())), - ("입출고 히스토리", Color.InvUse, Color.InvUseBg, AnyView(IncomingScanView())), - ("입고처리", Color.Transfer, Color.TransferBg, AnyView(IncomingScanView())), - ("사용처리", Color.InvStock, Color.InvStockBg, AnyView(IncomingScanView())), + ("재고 조회", true, "InvStock", AnyView(InventorySearchView())), + ("입출고 히스토리", false, "InvTrans", AnyView(IncomingScanView())), + ("입고 처리", false, "InvIncoming", AnyView(IncomingScanView())), + ("사용 처리", true, "InvUse", AnyView(IncomingScanView())), ] - + var body: some View { - VStack(spacing: 20) { + VStack(spacing: 15) { LazyVGrid( columns: [ - GridItem(.flexible(), spacing: 15), - GridItem(.flexible(), spacing: 15), + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10), ], - spacing: 15 + spacing: 10 ) { - ForEach(menuItems, id: \.0) { item in - ZStack { - // 그림자 전용 레이어 (NavigationLink 뒤에 위치) - RoundedRectangle(cornerRadius: 16) - .fill(Color.white) - .shadow( - color: .black.opacity(0.25), - radius: 4, - x: 0, - y: 4 - ) - - // 버튼 본체 - NavigationLink(destination: item.3) { - VStack(spacing: 12) { - Image("InvStock") - .renderingMode(.template) - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - .foregroundColor(item.1) - .padding(.top, 20) - - Text(item.0) - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(item.1) - .padding(.bottom, 20) + NavigationLink(destination: item.3) { + VStack(alignment: .leading, spacing: 19) { + HStack { + Spacer() + ZStack { + // 타원 배경 + Rectangle() + .fill(item.1 ? Color.white.opacity(0.2) : Color.Secondary) + .frame(width: 35, height: 26) + .cornerRadius(80) + + // 아이콘 + Image(item.2) + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .foregroundColor(.white) + } } - .frame(maxWidth: .infinity, minHeight: 120) - .background(item.2.opacity(0.5)) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(item.1, lineWidth: 1.2) - ) - .cornerRadius(16) + + Text(item.0) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(item.1 ? .white : Color.Secondary) } - .buttonStyle(PlainButtonStyle()) + .padding(.horizontal, 20) + .padding(.vertical, 20) + .frame(height: 99) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 24) + .fill(item.1 ? Color.Secondary : Color.white) + // 카드 그림자 (Figma 스펙: y=4, blur=4, opacity=25%, black) + .shadow(color: .black.opacity(0.35), radius: 2, x: 0, y: 4) + ) } + .buttonStyle(.plain) } } - .padding(.horizontal, 10) - } - .padding() - .background(Color.White) - .padding(.horizontal) - } -} - -struct StockShortageCard: View { - let partName: String - let carModel: String - let currentCount: Int - let minCount: Int - - var body: some View { - HStack(alignment: .center, spacing: 12) { - RoundedRectangle(cornerRadius: 8) - .fill(Color.gray.opacity(0.2)) - .frame(width: 68, height: 68) - - VStack(alignment: .leading, spacing: 4) { - Text(partName) - .font(.subheadline) - .fontWeight(.semibold) - Text(carModel) - .font(.subheadline) - .foregroundColor(.textGray1) - } - Spacer() - - VStack(alignment: .leading) { - Text("수량 부족") - .font(.system(size: 14, weight: .semibold)) - .fontWeight(.regular) - .foregroundColor(.StatusRed) - .padding(.vertical, 6) - .padding(.horizontal, 12) - .background(Color.StatusRedBg) - .cornerRadius(14) - - Text("현재수량: \(currentCount)개") - .font(.caption) - .foregroundColor(.textGray1) - - Text("최소수량: \(minCount)개") - .font(.caption) - .foregroundColor(.textGray1) - } + .padding(.horizontal, 15) } - .padding(12) - .background(Color.white) - .cornerRadius(12) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + .padding(.vertical, 17) } } diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index 6975c61..03e7979 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -20,6 +20,10 @@ final class InventoryViewModel: ObservableObject { @Published var currentPage = 0 @Published var isLoading = false @Published var hasMore = true + + @Published var underLimitItems: [InventoryItem] = [] + @Published var underLimitPage = 0 + @Published var underLimitHasMore = true private let repo: InventoryRepositoryProtocol @@ -27,6 +31,7 @@ final class InventoryViewModel: ObservableObject { self.repo = repo } + // 재고 조회 func loadInventoryList( reset: Bool = false, size: Int = 20 @@ -101,4 +106,64 @@ final class InventoryViewModel: ObservableObject { } Task { await resetAndLoad() } } + + // 부족 재고 조회 +// func loadUnderLimitList(categoryName: String? = nil) async { +// let result = await repo.getUnderLimitList( +// categoryName: categoryName, +// page: 0, +// size: 10 +// ) +// +// switch result { +// case .success(let apiResp): +// if let data = apiResp.data { +// underLimitItems = data.content +// } else { +// message = apiResp.message +// } +// case .failure(let err): +// message = err.message +// if err.code == 401 || err.code == 403 { +// shouldGoToLogin = true +// } +// } +// } + func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { + guard !isLoading, underLimitHasMore else { return } + isLoading = true + + if reset { + underLimitPage = 0 + underLimitItems.removeAll() + underLimitHasMore = true + } + + // 여기서 under-limit API 호출 + let result = await repo.getUnderLimitList( + categoryName: selectedCategories.first, // 필터 적용 시 사용 + page: underLimitPage, + size: size + ) + + switch result { + case .success(let apiResp): + if let data = apiResp.data { + if reset { + underLimitItems = data.content + } else { + underLimitItems.append(contentsOf: data.content) + } + underLimitHasMore = underLimitPage + 1 < data.totalPages + underLimitPage += 1 + } + case .failure(let err): + message = err.message + if err.code == 401 || err.code == 403 { + shouldGoToLogin = true + } + } + isLoading = false + } + } From f9f066c8932bf20fae08a57fd305f5f6f775cfb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Fri, 17 Oct 2025 12:35:32 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[FEAT]=20=EC=9E=AC=EA=B3=A0=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=83=AD=20=EB=B6=80=ED=92=88=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20UI?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/inventory/data/InventoryApi.swift | 8 + .../data/InventoryRepositoryImpl.swift | 12 + .../domain/InventoryRepositoryProtocol.swift | 8 + .../inventory/ui/InventorySearchView.swift | 212 ++++++++---------- .../viewmodel/InventoryViewModel.swift | 121 ++++++++-- 5 files changed, 223 insertions(+), 138 deletions(-) diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift index 2d250a9..ed33182 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift @@ -72,4 +72,12 @@ enum InventoryApi { } return ApiClient.shared.request(url, method: .get) } + + // ✅ 부품 이름으로 검색 + static func findByName(name: String, page: Int, size: Int) -> DataRequest { + let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let url = ApiClient.baseURL + "api/v1/store/find-name?name=\(encodedName)&page=\(page)&size=\(size)" + return ApiClient.shared.request(url, method: .get) + } + } diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift index cb66353..914e732 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift @@ -34,4 +34,16 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { let dataReq = InventoryApi.getUnderLimitList(categoryName: categoryName, page: page, size: size) return await safeApi(dataReq, decodeTo: ApiResponse.self) } + + // ✅ 이름 검색 + func findByName( + name: String, + page: Int, + size: Int + ) async -> AppResult> { + let dataReq = InventoryApi.findByName(name: name, page: page, size: size) + return await safeApi(dataReq, decodeTo: ApiResponse.self) + } + + } diff --git a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift index eb7a550..3d45edd 100644 --- a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift @@ -21,4 +21,12 @@ protocol InventoryRepositoryProtocol { page: Int, size: Int ) async -> AppResult> + + // 이름 검색 + func findByName( + name: String, + page: Int, + size: Int + ) async -> AppResult> + } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 0455f6f..ce8ac6c 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -12,6 +12,7 @@ struct InventorySearchView: View { @StateObject private var inventoryViewModel = InventoryViewModel() @State private var searchText = "" + private let categories = ["전기/램프", "엔진/미션", "하체/바디", "내장/외장", "기타소모품"] private let trims = ["준중형/소형", "중형", "대형", "SUV", "화물/트럭/승합", "수소/전기"] @@ -39,8 +40,24 @@ struct InventorySearchView: View { HStack { Image(systemName: "magnifyingglass") .foregroundColor(.gray) + TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) + .onChange(of: searchText) { newValue in + inventoryViewModel.searchInFilteredList(keyword: newValue) + } + + if !searchText.isEmpty { + Button(action: { + searchText = "" + inventoryViewModel.isSearching = false + }) { + Image(systemName: "xmark") + .foregroundColor(.gray) + } + .buttonStyle(.plain) + .padding(.trailing,3) + } } .padding() .background(Color(.white)) @@ -48,67 +65,79 @@ struct InventorySearchView: View { .padding(.horizontal) .padding(.vertical) - // 🔽 필터 버튼 - ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { FilterMenu( title: "카테고리", items: categories, - selected: { inventoryViewModel.selectedCategories.contains($0) }, + selectedItems: inventoryViewModel.selectedCategories, onTap: { inventoryViewModel.toggleCategory($0) } ) + FilterMenu( title: "분류", items: trims, - selected: { inventoryViewModel.selectedTrims.contains($0) }, + selectedItems: inventoryViewModel.selectedTrims, onTap: { inventoryViewModel.toggleTrim($0) } ) + FilterMenu( title: "모델", items: filteredModels, - selected: { inventoryViewModel.selectedModels.contains($0) }, + selectedItems: inventoryViewModel.selectedModels, onTap: { inventoryViewModel.toggleModel($0) } ) + + // 🔄 초기화 버튼 + Button(action: { + inventoryViewModel.resetFilters(with: searchText) + }) { + HStack(spacing: 4) { + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 13)) + Text("초기화") + .font(.system(size: 13, weight: .medium)) + } + .foregroundColor(.blue) + .padding(.trailing, 8) + } + .frame(maxWidth: .infinity, alignment: .trailing) + } .padding(.horizontal) - .padding(.top, 2) - .padding(.bottom, 4) - } - + .padding(.bottom, 16) + // 📋 재고 리스트 ScrollView { LazyVStack(spacing: 10) { - ForEach(inventoryViewModel.inventoryItems) { item in + + ForEach( + inventoryViewModel.isSearching + ? inventoryViewModel.filteredSearchResults // ✅ 검색 + 필터링 + : inventoryViewModel.inventoryItems + ) { item in InventoryCardView(item: item) .padding(.horizontal) .onAppear { - if item.id == inventoryViewModel.inventoryItems.last?.id, - inventoryViewModel.hasMore { - Task { - await inventoryViewModel.loadInventoryList() + if inventoryViewModel.isSearching { + if item.id == inventoryViewModel.searchResults.last?.id, + inventoryViewModel.searchHasMore { + Task { await inventoryViewModel.searchByName(name: searchText) } + } + } else { + if item.id == inventoryViewModel.inventoryItems.last?.id, + inventoryViewModel.hasMore { + Task { await inventoryViewModel.loadInventoryList() } } } } - - -// InventoryCard(item: item) -// .padding(.horizontal) -// .onAppear { -// if item.id == inventoryViewModel.inventoryItems.last?.id, -// inventoryViewModel.hasMore { -// Task { -// await inventoryViewModel.loadInventoryList() -// } -// } -// } } + if inventoryViewModel.isLoading && inventoryViewModel.hasMore { ProgressView() .padding(.vertical) } } - .padding(.top, 8) } } .background(Color.Light) @@ -120,93 +149,34 @@ struct InventorySearchView: View { } } -// ✅ 재고 카드 (UI 스타일 적용) -struct InventoryCard: View { - let item: InventoryItem - - var body: some View { - VStack(alignment: .leading, spacing: 5) { - Text(item.categoryName) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(.black) - - Divider() - .frame(height: 0.2) - .background(Color.textGray2) - - HStack(alignment: .center, spacing: 12) { - AsyncImage(url: URL(string: item.image)) { image in - image.resizable().scaledToFit() - } placeholder: { - Color.gray.opacity(0.2) - } - .frame(width: 64, height: 64) - .cornerRadius(10) - - VStack(alignment: .leading, spacing: 6) { - HStack { - Text(item.korName) - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.black) - .lineLimit(2) - Spacer() - } - .padding(.top, 2) - - Text("\(item.trim)/\(item.model)") - .font(.system(size: 13, weight: .regular)) - .foregroundColor(.gray) - - - - } - .frame(height: 60, alignment: .top) - - VStack (alignment: .center, spacing: 6) { - if item.isLack { - Text("수량 부족") - .font(.system(size: 13, weight: .regular)) - .padding(.horizontal, 10) - .padding(.vertical, 5) - .background(Color.DangerBg) - .foregroundColor(.Danger) - .cornerRadius(12) - } else { - Text("수량 여유") - .font(.system(size: 13, weight: .regular)) - .padding(.horizontal, 10) - .padding(.vertical, 5) - .background(Color.StatusGreenBg) - .foregroundColor(.StatusGreen) - .cornerRadius(12) - } - - VStack(alignment: .leading){ - Text("현재수량: \(item.amount)개") - .font(.system(size: 11, weight: .regular)) - .foregroundColor(.textGray1) - - Text("최소수량: \(item.limitAmount)개") - .font(.system(size: 11, weight: .regular)) - .foregroundColor(.textGray1) - } - } - } - } - .padding() - .background(Color.white) - .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) - } -} - // ✅ 필터 버튼 공용 컴포넌트 struct FilterMenu: View { let title: String let items: [String] - let selected: (String) -> Bool + let selectedItems: [String] let onTap: (String) -> Void + var displayTitle: String { + if selectedItems.isEmpty { + return title + } else if selectedItems.count == 1 { + return selectedItems.first ?? title + } else { + return "\(title) \(selectedItems.count)" + } + } + + var isActive: Bool { !selectedItems.isEmpty } + + var truncatedTitle: String { + // 글자 6자까지만 표시, 이후 "..." 처리 + if displayTitle.count > 6 { + let prefix = displayTitle.prefix(5) + return "\(prefix)…" + } + return displayTitle + } + var body: some View { Menu { ForEach(items, id: \.self) { item in @@ -215,21 +185,31 @@ struct FilterMenu: View { } label: { HStack { Text(item) - if selected(item) { + if selectedItems.contains(item) { Spacer() Image(systemName: "checkmark") + .foregroundColor(.blue) } } } } } label: { - Text(title) - .font(.subheadline) - .foregroundColor(.black) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color(.systemGray6)) - .cornerRadius(8) + HStack(spacing: 6) { + Text(truncatedTitle) + .font(.system(size: 13)) + .foregroundColor(isActive ? .blue : .black) + .lineLimit(1) + .truncationMode(.tail) // ✅ 안전하게 "..." 처리 + .multilineTextAlignment(.center) + + Image(systemName: "chevron.down") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(isActive ? .blue : .gray) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) // ✅ 높이 늘림 + .background(isActive ? Color.blue.opacity(0.2) : Color(.systemGray6)) + .cornerRadius(8) } } } diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index 03e7979..ee9c06e 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -24,6 +24,12 @@ final class InventoryViewModel: ObservableObject { @Published var underLimitItems: [InventoryItem] = [] @Published var underLimitPage = 0 @Published var underLimitHasMore = true + + @Published var searchResults: [InventoryItem] = [] + @Published var searchPage = 0 + @Published var searchHasMore = true + @Published var isSearching = false + private let repo: InventoryRepositoryProtocol @@ -37,6 +43,7 @@ final class InventoryViewModel: ObservableObject { size: Int = 20 ) async { guard !isLoading else { return } + isSearching = false // ✅ 검색 모드 해제 isLoading = true if reset { @@ -107,28 +114,7 @@ final class InventoryViewModel: ObservableObject { Task { await resetAndLoad() } } - // 부족 재고 조회 -// func loadUnderLimitList(categoryName: String? = nil) async { -// let result = await repo.getUnderLimitList( -// categoryName: categoryName, -// page: 0, -// size: 10 -// ) -// -// switch result { -// case .success(let apiResp): -// if let data = apiResp.data { -// underLimitItems = data.content -// } else { -// message = apiResp.message -// } -// case .failure(let err): -// message = err.message -// if err.code == 401 || err.code == 403 { -// shouldGoToLogin = true -// } -// } -// } + // 부족 재고 로드 func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { guard !isLoading, underLimitHasMore else { return } isLoading = true @@ -166,4 +152,95 @@ final class InventoryViewModel: ObservableObject { isLoading = false } + // ✅ 부품 이름 검색 + func searchByName(name: String, reset: Bool = false, size: Int = 20) async { + guard !isLoading else { return } + isLoading = true + isSearching = true + + if reset { + searchPage = 0 + searchResults.removeAll() + searchHasMore = true + } + + let result = await repo.findByName(name: name, page: searchPage, size: size) + + switch result { + case .success(let apiResp): + if let data = apiResp.data { + if reset { + searchResults = data.content + } else { + searchResults.append(contentsOf: data.content) + } + searchHasMore = searchPage + 1 < data.totalPages + searchPage += 1 + } + case .failure(let err): + message = err.message + if err.code == 401 || err.code == 403 { + shouldGoToLogin = true + } + } + + isLoading = false + } + + // 검색 후 필터링된 리스트 계산 + var filteredSearchResults: [InventoryItem] { + searchResults.filter { item in + // 카테고리 필터 + if !selectedCategories.isEmpty && !selectedCategories.contains(item.categoryName) { + return false + } + // 트림 필터 + if !selectedTrims.isEmpty && !selectedTrims.contains(item.trim) { + return false + } + // 모델 필터 + if !selectedModels.isEmpty && !selectedModels.contains(item.model) { + return false + } + return true + } + } + + func searchInFilteredList(keyword: String) { + guard !keyword.trimmingCharacters(in: .whitespaces).isEmpty else { + // 검색어 비면 원래 리스트 그대로 표시 + isSearching = false + return + } + + isSearching = true + searchResults = inventoryItems.filter { + //$0.name.localizedCaseInsensitiveContains(keyword) || + $0.korName.localizedCaseInsensitiveContains(keyword) // || + //$0.engName.localizedCaseInsensitiveContains(keyword) + } + } + + func resetFilters(with searchText: String) { + // 1. 필터 관련 선택 초기화 + selectedCategories.removeAll() + selectedTrims.removeAll() + selectedModels.removeAll() + + // 2. 검색어가 없을 경우 → 전체 재고 다시 + if searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + isSearching = false + searchResults.removeAll() + searchPage = 0 + Task { await loadInventoryList(reset: true) } + } + // 3. 검색어가 있는 경우 → 해당 검색어로 전체 결과 다시 검색 + else { + isSearching = true + searchPage = 0 + searchResults.removeAll() + Task { await searchByName(name: searchText, reset: true) } + } + } + } From 05d737530a06f34183ecd4365be047d99f48de73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Fri, 17 Oct 2025 17:31:16 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[FIX]=20=EC=9E=AC=EA=B3=A0=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=83=AD=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/feature/auth/ui/LoginView.swift | 3 +-- .../inventory/ui/InventorySearchView.swift | 8 +++--- .../feature/inventory/ui/InventoryView.swift | 25 +++++++++++-------- .../viewmodel/InventoryViewModel.swift | 3 ++- .../app/navigation/MainTabView.swift | 20 ++++++++++----- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index 06d745a..4409e9a 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -18,7 +18,6 @@ struct LoginView: View { @State private var emailError: String? = nil @State private var pwError: String? = nil -// var onLoginSuccess: () -> Void = {} var onLogin: (String, String) -> Void = { _, _ in } var onClickRegister: () -> Void = {} @@ -121,7 +120,7 @@ struct LoginView: View { // MARK: - 유효성 검사 함수 private func isValidForm() -> Bool { emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" - //pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" + pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" return emailError == nil //&& pwError == nil } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index ce8ac6c..0069d00 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -22,7 +22,7 @@ struct InventorySearchView: View { "대형": ["제네시스BH", "에쿠스"], "SUV": ["베뉴", "코나OS", "코나SX2", "투싼IX", "투싼TL", "투싼NX4", "싼타페CM", "싼타페DM", "싼타페TM", "싼타페MX5", "맥스크루즈", "베라크루즈", "팰리세이드LX2", "팰리세이드LX3"], "화물/트럭/승합": ["스타렉스", "그랜드스타렉스", "스타리아", "포터2", "쏠라티", "마이티", "메가트럭", "카운티"], - "수소/전기자동차": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] + "수소/전기": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] ] private var filteredModels: [String] { @@ -132,8 +132,10 @@ struct InventorySearchView: View { } } - - if inventoryViewModel.isLoading && inventoryViewModel.hasMore { + if inventoryViewModel.isLoading && + (inventoryViewModel.isSearching + ? inventoryViewModel.searchHasMore + : inventoryViewModel.hasMore) { ProgressView() .padding(.vertical) } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index 58ab734..b0d3132 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -7,9 +7,14 @@ import SwiftUI struct InventoryView: View { + @Binding var selectedTab: Int + @Binding var tabTappedTrigger: Bool + @StateObject private var inventoryViewModel = InventoryViewModel() @State private var showScrollToTopButton = false @Namespace private var topID + + @State private var lastTabSelection = 0 var body: some View { NavigationStack { @@ -17,12 +22,12 @@ struct InventoryView: View { ZStack { ScrollView { VStack(spacing: 0) { - // ✅ 스크롤 감지용 (맨 위) + // 스크롤 감지용 (맨 위) GeometryReader { geo in Color.clear .onChange(of: geo.frame(in: .global).minY) { newValue in // 👇 스크롤 시 값이 변함 - print("📏 Scroll offsetY:", newValue) // 테스트용, 화면 안정화 후 제거 + //print("📏 Scroll offsetY:", newValue) // 테스트용, 화면 안정화 후 제거 withAnimation(.easeInOut(duration: 0.25)) { showScrollToTopButton = newValue < -150 } @@ -84,13 +89,13 @@ struct InventoryView: View { } label: { ZStack { Circle() - .fill(Color.Secondary) // ✅ 배경색 + .fill(Color.Secondary) // 배경색 .frame(width: 50, height: 50) Image(systemName: "arrow.up") .font( .system(size: 24, weight: .bold) ) - .foregroundColor(.white) // ✅ 화살표 색 + .foregroundColor(.white) // 화살표 색 } } .padding(.trailing, 20) @@ -100,13 +105,17 @@ struct InventoryView: View { .transition(.opacity) .animation(.easeInOut(duration: 0.25), value: showScrollToTopButton) } - - } .background(Color.Light) .task { await inventoryViewModel.loadUnderLimitList(reset: true) } + // 같은 탭 다시 눌릴 때 위로 스크롤 + .onChange(of: tabTappedTrigger) { _ in + withAnimation(.easeInOut) { + proxy.scrollTo(topID, anchor: .top) + } + } } } } @@ -174,7 +183,3 @@ struct GridMenuView: View { .padding(.vertical, 17) } } - -#Preview { - InventoryView() -} diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index ee9c06e..dd8f3e5 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -116,7 +116,8 @@ final class InventoryViewModel: ObservableObject { // 부족 재고 로드 func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { - guard !isLoading, underLimitHasMore else { return } +// guard !isLoading, underLimitHasMore else { return } + guard !isLoading, (underLimitHasMore || reset) else { return } isLoading = true if reset { diff --git a/StockMate/StockMate/app/navigation/MainTabView.swift b/StockMate/StockMate/app/navigation/MainTabView.swift index ed4fcef..0b42bed 100644 --- a/StockMate/StockMate/app/navigation/MainTabView.swift +++ b/StockMate/StockMate/app/navigation/MainTabView.swift @@ -9,17 +9,17 @@ import SwiftUI struct MainTabView: View { @State private var selectedTab = 0 + @State private var tabTappedTrigger = false var body: some View { VStack(spacing: 0) { // 메인 화면 ZStack { switch selectedTab { - case 0: NavigationStack{ HomeView() -// UserInfoView() - } + case 0: NavigationStack{ HomeView() } case 1: NavigationStack{ OrderView() } - case 2: NavigationStack{ InventoryView() } + case 2: + NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) } case 3: NavigationStack{ ContentView() } default: NavigationStack{ HomeView() } } @@ -44,9 +44,17 @@ struct MainTabView: View { func tabButton(index: Int, icon: String, text: String) -> some View { let isSelected = selectedTab == index return Button { - withAnimation(.easeInOut) { // 탭 전환 애니메이션. 없애도 됨 - selectedTab = index + if selectedTab == index { + // ✅ 같은 탭 다시 누르면 트리거 토글 + tabTappedTrigger.toggle() + } else { + withAnimation(.easeInOut) { + selectedTab = index + } } +// withAnimation(.easeInOut) { // 탭 전환 애니메이션. 없애도 됨 +// selectedTab = index +// } } label: { VStack(spacing: 6) { Image(icon) From b20224031d6c99d0969747f529a32aa6e1bff4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Fri, 17 Oct 2025 17:44:10 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[FIX]=20=EC=9E=AC=EA=B3=A0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=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 --- .../inventory/ui/InventorySearchView.swift | 157 ++++++++++++------ 1 file changed, 106 insertions(+), 51 deletions(-) diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 0069d00..a2aa908 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -17,11 +17,54 @@ struct InventorySearchView: View { private let trims = ["준중형/소형", "중형", "대형", "SUV", "화물/트럭/승합", "수소/전기"] private let trimToModels: [String: [String]] = [ - "준중형/소형": ["아반떼MD", "아반떼AD", "아반떼CN7", "I30", "엑센트", "아이오닉", "벨로스터", "캐스퍼"], - "중형": ["NF소나타", "YF소나타", "LF소나타", "DN8소나타", "그랜저TG", "그랜저HG", "그랜저IG", "그랜저GN7", "I40"], + "준중형/소형": [ + "아반떼MD", + "아반떼AD", + "아반떼CN7", + "I30", + "엑센트", + "아이오닉", + "벨로스터", + "캐스퍼" + ], + "중형": [ + "NF소나타", + "YF소나타", + "LF소나타", + "DN8소나타", + "그랜저TG", + "그랜저HG", + "그랜저IG", + "그랜저GN7", + "I40" + ], "대형": ["제네시스BH", "에쿠스"], - "SUV": ["베뉴", "코나OS", "코나SX2", "투싼IX", "투싼TL", "투싼NX4", "싼타페CM", "싼타페DM", "싼타페TM", "싼타페MX5", "맥스크루즈", "베라크루즈", "팰리세이드LX2", "팰리세이드LX3"], - "화물/트럭/승합": ["스타렉스", "그랜드스타렉스", "스타리아", "포터2", "쏠라티", "마이티", "메가트럭", "카운티"], + "SUV": [ + "베뉴", + "코나OS", + "코나SX2", + "투싼IX", + "투싼TL", + "투싼NX4", + "싼타페CM", + "싼타페DM", + "싼타페TM", + "싼타페MX5", + "맥스크루즈", + "베라크루즈", + "팰리세이드LX2", + "팰리세이드LX3" + ], + "화물/트럭/승합": [ + "스타렉스", + "그랜드스타렉스", + "스타리아", + "포터2", + "쏠라티", + "마이티", + "메가트럭", + "카운티" + ], "수소/전기": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] ] @@ -29,7 +72,8 @@ struct InventorySearchView: View { if inventoryViewModel.selectedTrims.isEmpty { return trimToModels.values.flatMap { $0 } } else { - return inventoryViewModel.selectedTrims.flatMap { trimToModels[$0] ?? [] } + return inventoryViewModel.selectedTrims + .flatMap { trimToModels[$0] ?? [] } } } @@ -44,7 +88,8 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) .onChange(of: searchText) { newValue in - inventoryViewModel.searchInFilteredList(keyword: newValue) + inventoryViewModel + .searchInFilteredList(keyword: newValue) } if !searchText.isEmpty { @@ -65,46 +110,46 @@ struct InventorySearchView: View { .padding(.horizontal) .padding(.vertical) - HStack(spacing: 10) { - FilterMenu( - title: "카테고리", - items: categories, - selectedItems: inventoryViewModel.selectedCategories, - onTap: { inventoryViewModel.toggleCategory($0) } - ) + HStack(spacing: 10) { + FilterMenu( + title: "카테고리", + items: categories, + selectedItems: inventoryViewModel.selectedCategories, + onTap: { inventoryViewModel.toggleCategory($0) } + ) - FilterMenu( - title: "분류", - items: trims, - selectedItems: inventoryViewModel.selectedTrims, - onTap: { inventoryViewModel.toggleTrim($0) } - ) + FilterMenu( + title: "분류", + items: trims, + selectedItems: inventoryViewModel.selectedTrims, + onTap: { inventoryViewModel.toggleTrim($0) } + ) - FilterMenu( - title: "모델", - items: filteredModels, - selectedItems: inventoryViewModel.selectedModels, - onTap: { inventoryViewModel.toggleModel($0) } - ) + FilterMenu( + title: "모델", + items: filteredModels, + selectedItems: inventoryViewModel.selectedModels, + onTap: { inventoryViewModel.toggleModel($0) } + ) - // 🔄 초기화 버튼 - Button(action: { - inventoryViewModel.resetFilters(with: searchText) - }) { - HStack(spacing: 4) { - Image(systemName: "arrow.counterclockwise") - .font(.system(size: 13)) - Text("초기화") - .font(.system(size: 13, weight: .medium)) - } - .foregroundColor(.blue) - .padding(.trailing, 8) - } - .frame(maxWidth: .infinity, alignment: .trailing) - + // 🔄 초기화 버튼 + Button(action: { + inventoryViewModel.resetFilters(with: searchText) + }) { + HStack(spacing: 4) { + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 13)) + Text("초기화") + .font(.system(size: 13, weight: .medium)) + } + .foregroundColor(.blue) + .padding(.trailing, 8) } - .padding(.horizontal) - .padding(.bottom, 16) + .frame(maxWidth: .infinity, alignment: .trailing) + + } + .padding(.horizontal) + .padding(.bottom, 16) // 📋 재고 리스트 ScrollView { @@ -112,30 +157,38 @@ struct InventorySearchView: View { ForEach( inventoryViewModel.isSearching - ? inventoryViewModel.filteredSearchResults // ✅ 검색 + 필터링 - : inventoryViewModel.inventoryItems + ? inventoryViewModel.filteredSearchResults // ✅ 검색 + 필터링 + : inventoryViewModel.inventoryItems ) { item in InventoryCardView(item: item) .padding(.horizontal) .onAppear { if inventoryViewModel.isSearching { - if item.id == inventoryViewModel.searchResults.last?.id, + if item.id == inventoryViewModel.filteredSearchResults.last?.id, inventoryViewModel.searchHasMore { - Task { await inventoryViewModel.searchByName(name: searchText) } + Task { + await inventoryViewModel + .searchByName( + name: searchText + ) + } } } else { if item.id == inventoryViewModel.inventoryItems.last?.id, inventoryViewModel.hasMore { - Task { await inventoryViewModel.loadInventoryList() } + Task { + await inventoryViewModel + .loadInventoryList() + } } } } } if inventoryViewModel.isLoading && - (inventoryViewModel.isSearching - ? inventoryViewModel.searchHasMore - : inventoryViewModel.hasMore) { + (inventoryViewModel.isSearching + ? inventoryViewModel.searchHasMore + : inventoryViewModel.hasMore) { ProgressView() .padding(.vertical) } @@ -210,7 +263,9 @@ struct FilterMenu: View { } .padding(.horizontal, 12) .padding(.vertical, 10) // ✅ 높이 늘림 - .background(isActive ? Color.blue.opacity(0.2) : Color(.systemGray6)) + .background( + isActive ? Color.blue.opacity(0.2) : Color(.systemGray6) + ) .cornerRadius(8) } } From 3b00a938baed69c3f936bd7dd6995b7072e18ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Fri, 17 Oct 2025 17:59:18 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[FIX]=20=EC=9E=AC=EA=B3=A0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inventory/ui/InventorySearchView.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index a2aa908..32a9c4f 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -87,15 +87,23 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) +// .onChange(of: searchText) { newValue in +// inventoryViewModel +// .searchInFilteredList(keyword: newValue) +// } .onChange(of: searchText) { newValue in - inventoryViewModel - .searchInFilteredList(keyword: newValue) - } + let term = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + if term.isEmpty { + Task { await inventoryViewModel.loadInventoryList(reset: true) } + } else { + Task { await inventoryViewModel.searchByName(name: term, reset: true) } + } + } if !searchText.isEmpty { Button(action: { searchText = "" - inventoryViewModel.isSearching = false + //inventoryViewModel.isSearching = false }) { Image(systemName: "xmark") .foregroundColor(.gray) @@ -169,7 +177,7 @@ struct InventorySearchView: View { Task { await inventoryViewModel .searchByName( - name: searchText + name: searchText.trimmingCharacters(in: .whitespacesAndNewlines) ) } } @@ -224,7 +232,7 @@ struct FilterMenu: View { var isActive: Bool { !selectedItems.isEmpty } var truncatedTitle: String { - // 글자 6자까지만 표시, 이후 "..." 처리 + // 글자 5자까지만 표시, 이후 "..." 처리 if displayTitle.count > 6 { let prefix = displayTitle.prefix(5) return "\(prefix)…" From ad06779953a1d6caecf4e5e0a51b937215ae9944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Fri, 17 Oct 2025 18:39:09 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[FIX]=20=EC=9E=AC=EA=B3=A0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/feature/auth/ui/LoginView.swift | 2 +- .../inventory/ui/InventorySearchView.swift | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index 4409e9a..c504e96 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -120,7 +120,7 @@ struct LoginView: View { // MARK: - 유효성 검사 함수 private func isValidForm() -> Bool { emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" - pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" + //pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" return emailError == nil //&& pwError == nil } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 32a9c4f..8c394ad 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -87,23 +87,15 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) -// .onChange(of: searchText) { newValue in -// inventoryViewModel -// .searchInFilteredList(keyword: newValue) -// } .onChange(of: searchText) { newValue in - let term = newValue.trimmingCharacters(in: .whitespacesAndNewlines) - if term.isEmpty { - Task { await inventoryViewModel.loadInventoryList(reset: true) } - } else { - Task { await inventoryViewModel.searchByName(name: term, reset: true) } - } - } + inventoryViewModel + .searchInFilteredList(keyword: newValue) + } if !searchText.isEmpty { Button(action: { searchText = "" - //inventoryViewModel.isSearching = false + inventoryViewModel.isSearching = false }) { Image(systemName: "xmark") .foregroundColor(.gray) @@ -177,7 +169,10 @@ struct InventorySearchView: View { Task { await inventoryViewModel .searchByName( - name: searchText.trimmingCharacters(in: .whitespacesAndNewlines) + name: searchText + .trimmingCharacters( + in: .whitespacesAndNewlines + ) ) } } From b5325df450cc05250ebf6b7cd13bb6b9a160c4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Sun, 19 Oct 2025 13:46:49 +0900 Subject: [PATCH 08/11] =?UTF-8?q?Revert=20"[FIX]=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ad06779953a1d6caecf4e5e0a51b937215ae9944. --- .../app/feature/auth/ui/LoginView.swift | 2 +- .../inventory/ui/InventorySearchView.swift | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index c504e96..4409e9a 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -120,7 +120,7 @@ struct LoginView: View { // MARK: - 유효성 검사 함수 private func isValidForm() -> Bool { emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" - //pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" + pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" return emailError == nil //&& pwError == nil } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 8c394ad..32a9c4f 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -87,15 +87,23 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) +// .onChange(of: searchText) { newValue in +// inventoryViewModel +// .searchInFilteredList(keyword: newValue) +// } .onChange(of: searchText) { newValue in - inventoryViewModel - .searchInFilteredList(keyword: newValue) - } + let term = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + if term.isEmpty { + Task { await inventoryViewModel.loadInventoryList(reset: true) } + } else { + Task { await inventoryViewModel.searchByName(name: term, reset: true) } + } + } if !searchText.isEmpty { Button(action: { searchText = "" - inventoryViewModel.isSearching = false + //inventoryViewModel.isSearching = false }) { Image(systemName: "xmark") .foregroundColor(.gray) @@ -169,10 +177,7 @@ struct InventorySearchView: View { Task { await inventoryViewModel .searchByName( - name: searchText - .trimmingCharacters( - in: .whitespacesAndNewlines - ) + name: searchText.trimmingCharacters(in: .whitespacesAndNewlines) ) } } From 8185e13043cfcdb5c65c0dcd2ba34dd4b9d0aee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Sun, 19 Oct 2025 13:47:03 +0900 Subject: [PATCH 09/11] =?UTF-8?q?Revert=20"[FIX]=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3b00a938baed69c3f936bd7dd6995b7072e18ee7. --- .../inventory/ui/InventorySearchView.swift | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 32a9c4f..a2aa908 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -87,23 +87,15 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) -// .onChange(of: searchText) { newValue in -// inventoryViewModel -// .searchInFilteredList(keyword: newValue) -// } .onChange(of: searchText) { newValue in - let term = newValue.trimmingCharacters(in: .whitespacesAndNewlines) - if term.isEmpty { - Task { await inventoryViewModel.loadInventoryList(reset: true) } - } else { - Task { await inventoryViewModel.searchByName(name: term, reset: true) } - } - } + inventoryViewModel + .searchInFilteredList(keyword: newValue) + } if !searchText.isEmpty { Button(action: { searchText = "" - //inventoryViewModel.isSearching = false + inventoryViewModel.isSearching = false }) { Image(systemName: "xmark") .foregroundColor(.gray) @@ -177,7 +169,7 @@ struct InventorySearchView: View { Task { await inventoryViewModel .searchByName( - name: searchText.trimmingCharacters(in: .whitespacesAndNewlines) + name: searchText ) } } @@ -232,7 +224,7 @@ struct FilterMenu: View { var isActive: Bool { !selectedItems.isEmpty } var truncatedTitle: String { - // 글자 5자까지만 표시, 이후 "..." 처리 + // 글자 6자까지만 표시, 이후 "..." 처리 if displayTitle.count > 6 { let prefix = displayTitle.prefix(5) return "\(prefix)…" From 8b8b5ba6cf53371f42757705e5822b7849c07502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Mon, 20 Oct 2025 12:40:14 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[FEAT]=20=EC=9E=AC=EA=B3=A0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/feature/auth/ui/LoginView.swift | 2 +- .../inventory/ui/InventorySearchView.swift | 126 ++++++------------ .../viewmodel/InventoryViewModel.swift | 126 +++++++++++------- 3 files changed, 121 insertions(+), 133 deletions(-) diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index 4409e9a..06e87e3 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -121,7 +121,7 @@ struct LoginView: View { private func isValidForm() -> Bool { emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" - return emailError == nil //&& pwError == nil + return emailError == nil && pwError == nil } } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index a2aa908..5e8c205 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -12,59 +12,15 @@ struct InventorySearchView: View { @StateObject private var inventoryViewModel = InventoryViewModel() @State private var searchText = "" - + // 카테고리, 분류, 모델 private let categories = ["전기/램프", "엔진/미션", "하체/바디", "내장/외장", "기타소모품"] private let trims = ["준중형/소형", "중형", "대형", "SUV", "화물/트럭/승합", "수소/전기"] - private let trimToModels: [String: [String]] = [ - "준중형/소형": [ - "아반떼MD", - "아반떼AD", - "아반떼CN7", - "I30", - "엑센트", - "아이오닉", - "벨로스터", - "캐스퍼" - ], - "중형": [ - "NF소나타", - "YF소나타", - "LF소나타", - "DN8소나타", - "그랜저TG", - "그랜저HG", - "그랜저IG", - "그랜저GN7", - "I40" - ], + "준중형/소형": [ "아반떼MD", "아반떼AD", "아반떼CN7", "I30", "엑센트", "아이오닉", "벨로스터", "캐스퍼" ], + "중형": [ "NF소나타", "YF소나타", "LF소나타", "DN8소나타", "그랜저TG", "그랜저HG", "그랜저IG", "그랜저GN7", "I40" ], "대형": ["제네시스BH", "에쿠스"], - "SUV": [ - "베뉴", - "코나OS", - "코나SX2", - "투싼IX", - "투싼TL", - "투싼NX4", - "싼타페CM", - "싼타페DM", - "싼타페TM", - "싼타페MX5", - "맥스크루즈", - "베라크루즈", - "팰리세이드LX2", - "팰리세이드LX3" - ], - "화물/트럭/승합": [ - "스타렉스", - "그랜드스타렉스", - "스타리아", - "포터2", - "쏠라티", - "마이티", - "메가트럭", - "카운티" - ], + "SUV": [ "베뉴", "코나OS", "코나SX2", "투싼IX", "투싼TL", "투싼NX4", "싼타페CM", "싼타페DM", "싼타페TM", "싼타페MX5", "맥스크루즈", "베라크루즈", "팰리세이드LX2", "팰리세이드LX3" ], + "화물/트럭/승합": [ "스타렉스", "그랜드스타렉스", "스타리아", "포터2", "쏠라티", "마이티", "메가트럭", "카운티" ], "수소/전기": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] ] @@ -87,9 +43,10 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) - .onChange(of: searchText) { newValue in - inventoryViewModel - .searchInFilteredList(keyword: newValue) + .onSubmit { + Task { + await inventoryViewModel.searchByName(name: searchText, reset: true) + } } if !searchText.isEmpty { @@ -107,9 +64,14 @@ struct InventorySearchView: View { .padding() .background(Color(.white)) .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(Color.gray.opacity(0.4), lineWidth: 1) + ) .padding(.horizontal) .padding(.vertical) + // 필터 및 초기화 버튼 HStack(spacing: 10) { FilterMenu( title: "카테고리", @@ -138,10 +100,9 @@ struct InventorySearchView: View { }) { HStack(spacing: 4) { Image(systemName: "arrow.counterclockwise") - .font(.system(size: 13)) Text("초기화") - .font(.system(size: 13, weight: .medium)) } + .font(.system(size: 13, weight: .medium)) .foregroundColor(.blue) .padding(.trailing, 8) } @@ -157,41 +118,32 @@ struct InventorySearchView: View { ForEach( inventoryViewModel.isSearching - ? inventoryViewModel.filteredSearchResults // ✅ 검색 + 필터링 + ? inventoryViewModel.filteredSearchResults // 검색 + 필터링 : inventoryViewModel.inventoryItems ) { item in InventoryCardView(item: item) .padding(.horizontal) .onAppear { - if inventoryViewModel.isSearching { - if item.id == inventoryViewModel.filteredSearchResults.last?.id, - inventoryViewModel.searchHasMore { - Task { - await inventoryViewModel - .searchByName( - name: searchText - ) + Task { + if inventoryViewModel.isSearching { + if item.id == inventoryViewModel.filteredSearchResults.last?.id, + inventoryViewModel.searchHasMore { + await inventoryViewModel.loadMore(searchText: searchText) } - } - } else { - if item.id == inventoryViewModel.inventoryItems.last?.id, - inventoryViewModel.hasMore { - Task { - await inventoryViewModel - .loadInventoryList() + } else { + if item.id == inventoryViewModel.inventoryItems.last?.id, + inventoryViewModel.hasMore { + await inventoryViewModel.loadMore(searchText: searchText) } } } } } - if inventoryViewModel.isLoading && - (inventoryViewModel.isSearching - ? inventoryViewModel.searchHasMore - : inventoryViewModel.hasMore) { - ProgressView() - .padding(.vertical) - } + if inventoryViewModel.isLoading { + ProgressView() + .padding(.vertical) + } } } } @@ -204,7 +156,7 @@ struct InventorySearchView: View { } } -// ✅ 필터 버튼 공용 컴포넌트 +// 필터 버튼 공용 컴포넌트 struct FilterMenu: View { let title: String let items: [String] @@ -217,7 +169,7 @@ struct FilterMenu: View { } else if selectedItems.count == 1 { return selectedItems.first ?? title } else { - return "\(title) \(selectedItems.count)" + return "\(title) (\(selectedItems.count))" } } @@ -225,8 +177,8 @@ struct FilterMenu: View { var truncatedTitle: String { // 글자 6자까지만 표시, 이후 "..." 처리 - if displayTitle.count > 6 { - let prefix = displayTitle.prefix(5) + if displayTitle.count > 8 { + let prefix = displayTitle.prefix(8) return "\(prefix)…" } return displayTitle @@ -250,19 +202,19 @@ struct FilterMenu: View { } } label: { HStack(spacing: 6) { + Image(systemName: "chevron.down") + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(isActive ? .blue : .gray) + Text(truncatedTitle) .font(.system(size: 13)) .foregroundColor(isActive ? .blue : .black) .lineLimit(1) - .truncationMode(.tail) // ✅ 안전하게 "..." 처리 + .truncationMode(.tail) // 안전하게 "..." 처리 .multilineTextAlignment(.center) - - Image(systemName: "chevron.down") - .font(.system(size: 11, weight: .semibold)) - .foregroundColor(isActive ? .blue : .gray) } .padding(.horizontal, 12) - .padding(.vertical, 10) // ✅ 높이 늘림 + .padding(.vertical, 10) // 높이 늘림 .background( isActive ? Color.blue.opacity(0.2) : Color(.systemGray6) ) diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index dd8f3e5..b0ec3cf 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -9,42 +9,46 @@ import SwiftUI @MainActor final class InventoryViewModel: ObservableObject { + // ===== 기본 상태 ===== @Published var inventoryItems: [InventoryItem] = [] @Published var message: String = "" @Published var shouldGoToLogin: Bool = false - + + // ====== 필터 상태 ====== @Published var selectedCategories: [String] = [] @Published var selectedTrims: [String] = [] @Published var selectedModels: [String] = [] + + // ===== 전체 재고 페이지네이션 ===== @Published var currentPage = 0 - @Published var isLoading = false @Published var hasMore = true + // ====== 부족 재고 페이지네이션 ====== @Published var underLimitItems: [InventoryItem] = [] @Published var underLimitPage = 0 @Published var underLimitHasMore = true + // ====== 검색 관련 ====== @Published var searchResults: [InventoryItem] = [] @Published var searchPage = 0 @Published var searchHasMore = true @Published var isSearching = false - - + + // ====== 공통 상태 ====== + @Published var isLoading = false + private let repo: InventoryRepositoryProtocol init(repo: InventoryRepositoryProtocol = InventoryRepositoryImpl()) { self.repo = repo } - // 재고 조회 - func loadInventoryList( - reset: Bool = false, - size: Int = 20 - ) async { + // MARK: - 전체 재고 로드 + func loadInventoryList(reset: Bool = false, size: Int = 20) async { guard !isLoading else { return } - isSearching = false // ✅ 검색 모드 해제 isLoading = true + isSearching = false // 검색 모드 해제 if reset { currentPage = 0 @@ -81,40 +85,13 @@ final class InventoryViewModel: ObservableObject { } isLoading = false } - + + func resetAndLoad() async { await loadInventoryList(reset: true) } - - // MARK: - Filter toggle - func toggleCategory(_ name: String) { - if selectedCategories.contains(name) { - selectedCategories.removeAll { $0 == name } - } else { - selectedCategories.append(name) - } - Task { await resetAndLoad() } - } - - func toggleTrim(_ trim: String) { - if selectedTrims.contains(trim) { - selectedTrims.removeAll { $0 == trim } - } else { - selectedTrims.append(trim) - } - Task { await resetAndLoad() } - } - - func toggleModel(_ model: String) { - if selectedModels.contains(model) { - selectedModels.removeAll { $0 == model } - } else { - selectedModels.append(model) - } - Task { await resetAndLoad() } - } - // 부족 재고 로드 + // MARK: - 부족 재고 로드 func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { // guard !isLoading, underLimitHasMore else { return } guard !isLoading, (underLimitHasMore || reset) else { return } @@ -153,7 +130,7 @@ final class InventoryViewModel: ObservableObject { isLoading = false } - // ✅ 부품 이름 검색 + // MARK: - 이름 검색 func searchByName(name: String, reset: Bool = false, size: Int = 20) async { guard !isLoading else { return } isLoading = true @@ -188,7 +165,7 @@ final class InventoryViewModel: ObservableObject { isLoading = false } - // 검색 후 필터링된 리스트 계산 + // MARK: - 검색 결과에서 로컬 필터링 var filteredSearchResults: [InventoryItem] { searchResults.filter { item in // 카테고리 필터 @@ -207,13 +184,13 @@ final class InventoryViewModel: ObservableObject { } } + // MARK: - 검색어 입력 시 로컬 검색 전환 func searchInFilteredList(keyword: String) { guard !keyword.trimmingCharacters(in: .whitespaces).isEmpty else { // 검색어 비면 원래 리스트 그대로 표시 isSearching = false return } - isSearching = true searchResults = inventoryItems.filter { //$0.name.localizedCaseInsensitiveContains(keyword) || @@ -221,7 +198,56 @@ final class InventoryViewModel: ObservableObject { //$0.engName.localizedCaseInsensitiveContains(keyword) } } + + // MARK: - 필터 토글 (검색모드 해제 + 전체 재로드) + func toggleCategory(_ name: String) { + if selectedCategories.contains(name) { + selectedCategories.removeAll { $0 == name } + } else { + selectedCategories.append(name) + } + // ✅ 검색 중일 때는 로컬 필터링만 다시 계산 + if isSearching { + objectWillChange.send() + } else { + Task { await resetAndLoad() } + } + +// isSearching = false +// Task { await resetAndLoad() } + } + + func toggleTrim(_ trim: String) { + if selectedTrims.contains(trim) { + selectedTrims.removeAll { $0 == trim } + } else { + selectedTrims.append(trim) + } + if isSearching { + objectWillChange.send() + } else { + Task { await resetAndLoad() } + } +// isSearching = false +// Task { await resetAndLoad() } + } + func toggleModel(_ model: String) { + if selectedModels.contains(model) { + selectedModels.removeAll { $0 == model } + } else { + selectedModels.append(model) + } + if isSearching { + objectWillChange.send() + } else { + Task { await resetAndLoad() } + } +// isSearching = false +// Task { await resetAndLoad() } + } + + // MARK: - 필터 초기화 func resetFilters(with searchText: String) { // 1. 필터 관련 선택 초기화 selectedCategories.removeAll() @@ -232,16 +258,26 @@ final class InventoryViewModel: ObservableObject { if searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { isSearching = false searchResults.removeAll() - searchPage = 0 + // searchPage = 0 Task { await loadInventoryList(reset: true) } } // 3. 검색어가 있는 경우 → 해당 검색어로 전체 결과 다시 검색 else { isSearching = true - searchPage = 0 + //searchPage = 0 searchResults.removeAll() Task { await searchByName(name: searchText, reset: true) } } } + + // MARK: - ✅ 무한 스크롤 로드 + func loadMore(searchText: String) async { + if isSearching { + guard !searchText.trimmingCharacters(in: .whitespaces).isEmpty else { return } + await searchByName(name: searchText) + } else { + await loadInventoryList() + } + } } From 910feb503528f58e2bee962f8d37b856c6868dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=ED=98=84=EC=95=84?= Date: Mon, 20 Oct 2025 12:52:22 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[FIX]=20=EC=9E=AC=EA=B3=A0=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=96=B4=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inventory/ui/InventorySearchView.swift | 5 +++- .../viewmodel/InventoryViewModel.swift | 25 ++++++------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 5e8c205..000cc5c 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -44,8 +44,11 @@ struct InventorySearchView: View { TextField("부품을 검색하세요.", text: $searchText) .textFieldStyle(PlainTextFieldStyle()) .onSubmit { + let term = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !term.isEmpty else { return } Task { - await inventoryViewModel.searchByName(name: searchText, reset: true) +// await inventoryViewModel.searchByName(name: searchText, reset: true) + await inventoryViewModel.searchByName(name: term, reset: true) } } diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index b0ec3cf..6467147 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -91,7 +91,7 @@ final class InventoryViewModel: ObservableObject { await loadInventoryList(reset: true) } - // MARK: - 부족 재고 로드 + // MARK: - 부족 재고 로드 func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { // guard !isLoading, underLimitHasMore else { return } guard !isLoading, (underLimitHasMore || reset) else { return } @@ -184,21 +184,6 @@ final class InventoryViewModel: ObservableObject { } } - // MARK: - 검색어 입력 시 로컬 검색 전환 - func searchInFilteredList(keyword: String) { - guard !keyword.trimmingCharacters(in: .whitespaces).isEmpty else { - // 검색어 비면 원래 리스트 그대로 표시 - isSearching = false - return - } - isSearching = true - searchResults = inventoryItems.filter { - //$0.name.localizedCaseInsensitiveContains(keyword) || - $0.korName.localizedCaseInsensitiveContains(keyword) // || - //$0.engName.localizedCaseInsensitiveContains(keyword) - } - } - // MARK: - 필터 토글 (검색모드 해제 + 전체 재로드) func toggleCategory(_ name: String) { if selectedCategories.contains(name) { @@ -266,7 +251,13 @@ final class InventoryViewModel: ObservableObject { isSearching = true //searchPage = 0 searchResults.removeAll() - Task { await searchByName(name: searchText, reset: true) } +// Task { await searchByName(name: searchText, reset: true) } + Task { + await searchByName( + name: searchText.trimmingCharacters(in: .whitespacesAndNewlines), + reset: true + ) + } } }