diff --git a/CouponWallet.xcodeproj/project.pbxproj b/CouponWallet.xcodeproj/project.pbxproj index 4586136..26c8258 100644 --- a/CouponWallet.xcodeproj/project.pbxproj +++ b/CouponWallet.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 54A74DE32D7E779200BCBE47 /* Exceptions for "CouponWallet" folder in "CouponWallet" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - info.plist, + Resources/info.plist, ); target = 549BDD392D79259100B64F07 /* CouponWallet */; }; @@ -91,7 +91,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 1640; TargetAttributes = { 549BDD392D79259100B64F07 = { CreatedOnToolsVersion = 16.2; @@ -173,6 +173,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5R33532VPH; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -236,6 +237,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5R33532VPH; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -264,8 +266,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"CouponWallet/Preview Content\""; - DEVELOPMENT_TEAM = 5R33532VPH; + DEVELOPMENT_ASSET_PATHS = "\"CouponWallet/Resources/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = CouponWallet; @@ -283,9 +284,13 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = kr.co.youngmin.CouponWallet; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -296,8 +301,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"CouponWallet/Preview Content\""; - DEVELOPMENT_TEAM = 5R33532VPH; + DEVELOPMENT_ASSET_PATHS = "\"CouponWallet/Resources/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = CouponWallet; @@ -315,9 +319,13 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = kr.co.youngmin.CouponWallet; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/CouponWallet.xcodeproj/project.xcworkspace/xcuserdata/choyoungmin.xcuserdatad/UserInterfaceState.xcuserstate b/CouponWallet.xcodeproj/project.xcworkspace/xcuserdata/choyoungmin.xcuserdatad/UserInterfaceState.xcuserstate index 8af8f16..8062a3f 100644 Binary files a/CouponWallet.xcodeproj/project.xcworkspace/xcuserdata/choyoungmin.xcuserdatad/UserInterfaceState.xcuserstate and b/CouponWallet.xcodeproj/project.xcworkspace/xcuserdata/choyoungmin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/CouponWallet/CouponWalletApp.swift b/CouponWallet/App/CouponWalletApp.swift similarity index 100% rename from CouponWallet/CouponWalletApp.swift rename to CouponWallet/App/CouponWalletApp.swift diff --git a/CouponWallet/Gifticon.swift b/CouponWallet/Models/Gifticon.swift similarity index 86% rename from CouponWallet/Gifticon.swift rename to CouponWallet/Models/Gifticon.swift index 70398a5..07b0f33 100644 --- a/CouponWallet/Gifticon.swift +++ b/CouponWallet/Models/Gifticon.swift @@ -16,14 +16,16 @@ class Gifticon { var productName: String var imagePath: String var isUsed: Bool + var category: String - init(brand: String, productName: String, expirationDate: Date, isUsed: Bool, imagePath: String) { + init(brand: String, productName: String, expirationDate: Date, isUsed: Bool, imagePath: String, category: String) { self.id = UUID() self.brand = brand self.productName = productName self.expirationDate = expirationDate self.isUsed = isUsed self.imagePath = imagePath + self.category = category } var formattedExpiryDate: String { diff --git a/CouponWallet/Assets.xcassets/AccentColor.colorset/Contents.json b/CouponWallet/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from CouponWallet/Assets.xcassets/AccentColor.colorset/Contents.json rename to CouponWallet/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg b/CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg new file mode 100644 index 0000000..b5e4f80 Binary files /dev/null and b/CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.jpg differ diff --git a/CouponWallet/Assets.xcassets/AppIcon.appiconset/Contents.json b/CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 94% rename from CouponWallet/Assets.xcassets/AppIcon.appiconset/Contents.json rename to CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..630bd1f 100644 --- a/CouponWallet/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/CouponWallet/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon.jpg", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/CouponWallet/Assets.xcassets/Contents.json b/CouponWallet/Resources/Assets.xcassets/Contents.json similarity index 100% rename from CouponWallet/Assets.xcassets/Contents.json rename to CouponWallet/Resources/Assets.xcassets/Contents.json diff --git a/CouponWallet/Preview Content/Preview Assets.xcassets/Contents.json b/CouponWallet/Resources/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from CouponWallet/Preview Content/Preview Assets.xcassets/Contents.json rename to CouponWallet/Resources/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/CouponWallet/info.plist b/CouponWallet/Resources/info.plist similarity index 100% rename from CouponWallet/info.plist rename to CouponWallet/Resources/info.plist diff --git a/CouponWallet/CaptureManager.swift b/CouponWallet/Service/CaptureManager.swift similarity index 100% rename from CouponWallet/CaptureManager.swift rename to CouponWallet/Service/CaptureManager.swift diff --git a/CouponWallet/GfiticonScanner.swift b/CouponWallet/Service/GfiticonScanner.swift similarity index 91% rename from CouponWallet/GfiticonScanner.swift rename to CouponWallet/Service/GfiticonScanner.swift index 61abe5b..506bfd4 100644 --- a/CouponWallet/GfiticonScanner.swift +++ b/CouponWallet/Service/GfiticonScanner.swift @@ -9,6 +9,7 @@ struct ScanResult { var expirationDate: Date = TextAnalyzer.defaultExpirationDate var imagePath: String = "" var imageData: Data? = nil + var category: String = "기타" } struct TextAnalyzer { @@ -418,27 +419,47 @@ class GifticonScanManager: ObservableObject { // MARK: - 브랜드 추출 (개선됨) private func extractBrand(from texts: [String], pairs: [String: String]) { - // 1. 레이블-값 쌍에서 브랜드 찾기 if let exchange = pairs["교환처"], !exchange.isEmpty { scanResult.brand = exchange + scanResult.category = category(for: exchange) return } - // 2. 텍스트에서 브랜드 키워드 찾기 for text in texts { for brand in brandKeywords { if text.lowercased().contains(brand.lowercased()) { scanResult.brand = brand + scanResult.category = category(for: brand) return } } } - - // 3. 브랜드를 찾지 못했을 경우 기본값 설정 + if scanResult.brand.isEmpty { scanResult.brand = "기타" + scanResult.category = "기타" + } else { + scanResult.category = category(for: scanResult.brand) } } + + // 브랜드명에 따른 카테고리 반환 + private func category(for brand: String) -> String { + let convenienceStores = ["CU", "GS25", "세븐일레븐"] + let cafes = ["스타벅스", "이디야", "투썸플레이스", "빽다방", "메가커피"] + let chickenBrands = ["BBQ", "BHC", "교촌", "굽네치킨", "네네치킨"] + + if convenienceStores.contains(where: { brand.localizedCaseInsensitiveContains($0) }) { + return "편의점" + } else if cafes.contains(where: { brand.localizedCaseInsensitiveContains($0) }) { + return "카페" + } else if chickenBrands.contains(where: { brand.localizedCaseInsensitiveContains($0) }) { + return "치킨" + } else { + return "기타" + } + } + // MARK: - 유효기간 추출 private func extractExpirationDate(from texts: [String], pairs: [String: String]) { @@ -609,3 +630,36 @@ extension UIView { } } } + +extension AvailableGifticonView { + // 이미지를 Documents 디렉토리에 저장하고 파일명 반환 + private func saveImageToDocuments(_ image: UIImage) -> String? { + guard let imageData = image.jpegData(compressionQuality: 0.8) else { return nil } + + // Documents 디렉토리 경로 가져오기 + let documentsPath = FileManager.default.urls(for: .documentDirectory, + in: .userDomainMask)[0] + + // 유니크한 파일명 생성 + let fileName = UUID().uuidString + ".jpg" + let fileURL = documentsPath.appendingPathComponent(fileName) + + do { + try imageData.write(to: fileURL) + return fileName // 파일명만 반환 (전체 경로 아님) + } catch { + print("이미지 저장 실패: \(error)") + return nil + } + } + + // 수정된 이미지 저장 및 경로 반환 함수 + private func saveImageAndGetPath(_ image: UIImage, manager: GifticonScanManager) -> String { + // Documents 디렉토리에 저장 + if let fileName = saveImageToDocuments(image) { + return fileName + } + return "" + } +} + diff --git a/CouponWallet/CameraScanView.swift b/CouponWallet/Views/Camera/CameraScanView.swift similarity index 96% rename from CouponWallet/CameraScanView.swift rename to CouponWallet/Views/Camera/CameraScanView.swift index 1455d8f..577fafe 100644 --- a/CouponWallet/CameraScanView.swift +++ b/CouponWallet/Views/Camera/CameraScanView.swift @@ -76,7 +76,8 @@ struct ScanResultView: View { productName: productName, expirationDate: expirationDate, isUsed: false, - imagePath: imagePath + imagePath: imagePath, + category: scanManager.scanResult.category ) modelContext.insert(newGifticon) diff --git a/CouponWallet/ContentView.swift b/CouponWallet/Views/ContentView.swift similarity index 96% rename from CouponWallet/ContentView.swift rename to CouponWallet/Views/ContentView.swift index f8209bc..7544c93 100644 --- a/CouponWallet/ContentView.swift +++ b/CouponWallet/Views/ContentView.swift @@ -53,7 +53,7 @@ enum GifticonType { // 사용 가능한 기프티콘 뷰 struct AvailableGifticonView: View { @State private var selectedFilter = "전체" - let filters = ["전체", "스타벅스", "치킨", "CU", "GS25", "기타"] + let filters = ["전체", "편의점", "카페", "치킨", "기타"] @Query private var availableGifticons: [Gifticon] @Environment(\.modelContext) private var modelContext @@ -81,10 +81,11 @@ struct AvailableGifticonView: View { // 필터링 로직을 별도 함수로 분리 private func getFilteredGifticons() -> [Gifticon] { - if selectedFilter == "전체" { - return availableGifticons - } else { - return availableGifticons.filter { $0.brand == selectedFilter } + switch selectedFilter { + case "전체": + return availableGifticons + default: + return availableGifticons.filter { $0.category == selectedFilter } } } @@ -329,7 +330,8 @@ struct AvailableGifticonView: View { productName: scanManager.scanResult.productName, expirationDate: scanManager.scanResult.expirationDate, isUsed: false, - imagePath: imagePath + imagePath: imagePath, + category: scanManager.scanResult.category ) modelContext.insert(newGifticon) @@ -343,7 +345,8 @@ struct AvailableGifticonView: View { productName: "기프티콘", expirationDate: Date().addingTimeInterval(30*24*60*60), // 30일 후 만료 isUsed: false, - imagePath: imagePath + imagePath: imagePath, + category: "기타" ) modelContext.insert(newGifticon) diff --git a/CouponWallet/ExpiredView.swift b/CouponWallet/Views/Coupon/ExpiredView.swift similarity index 85% rename from CouponWallet/ExpiredView.swift rename to CouponWallet/Views/Coupon/ExpiredView.swift index eea9990..6fc7f36 100644 --- a/CouponWallet/ExpiredView.swift +++ b/CouponWallet/Views/Coupon/ExpiredView.swift @@ -198,28 +198,46 @@ struct GifticonCard: View { let status: String? @Environment(\.colorScheme) var colorScheme + // 로컬 이미지 로드 함수 + private func loadLocalImage(from fileName: String) -> UIImage? { + guard !fileName.isEmpty else { return nil } + + let documentsPath = FileManager.default.urls(for: .documentDirectory, + in: .userDomainMask)[0] + let fileURL = documentsPath.appendingPathComponent(fileName) + + if FileManager.default.fileExists(atPath: fileURL.path) { + return UIImage(contentsOfFile: fileURL.path) + } + return nil + } + var body: some View { VStack(alignment: .leading, spacing: 8) { ZStack { + // 이미지 표시 로직 수정 if !gifticon.imagePath.isEmpty { - AsyncImage(url: URL(string: gifticon.imagePath)) { image in - image.resizable() + // 로컬 이미지 먼저 시도 + if let localImage = loadLocalImage(from: gifticon.imagePath) { + Image(uiImage: localImage) + .resizable() .aspectRatio(contentMode: .fit) - } placeholder: { - Color.gray.opacity(0.1) + .frame(height: 100) + } else { + // URL 이미지 시도 (기존 코드) + AsyncImage(url: URL(string: gifticon.imagePath)) { image in + image.resizable() + .aspectRatio(contentMode: .fit) + } placeholder: { + defaultImagePlaceholder + } + .frame(height: 100) } - .frame(height: 100) } else { - Rectangle() - .fill(Color.gray.opacity(0.1)) - .frame(height: 100) - .overlay( - Text(gifticon.brand) - .foregroundColor(.gray) - ) + defaultImagePlaceholder } - // "available"이 아닐 때만 상태 표시 + // 상태 표시 (기존 코드와 동일) if let status = status, status != "available" && !status.isEmpty { Text(status) .font(.caption) @@ -248,6 +266,17 @@ struct GifticonCard: View { .cornerRadius(12) .shadow(color: colorScheme == .dark ? Color.clear : Color.black.opacity(0.05), radius: 5, x: 0, y: 2) } + + // 기본 이미지 플레이스홀더 + private var defaultImagePlaceholder: some View { + Rectangle() + .fill(Color.gray.opacity(0.1)) + .frame(height: 100) + .overlay( + Text(gifticon.brand) + .foregroundColor(.gray) + ) + } } extension Int { diff --git a/CouponWallet/SelectExpiredCouponView.swift b/CouponWallet/Views/Coupon/SelectExpiredCouponView.swift similarity index 100% rename from CouponWallet/SelectExpiredCouponView.swift rename to CouponWallet/Views/Coupon/SelectExpiredCouponView.swift diff --git a/CouponWallet/SelectedCouponView.swift b/CouponWallet/Views/Coupon/SelectedCouponView.swift similarity index 87% rename from CouponWallet/SelectedCouponView.swift rename to CouponWallet/Views/Coupon/SelectedCouponView.swift index 5301809..b89700b 100644 --- a/CouponWallet/SelectedCouponView.swift +++ b/CouponWallet/Views/Coupon/SelectedCouponView.swift @@ -171,24 +171,41 @@ struct SelectedCouponView: View { struct SelectedCouponCell: View { var selectedCoupon: Gifticon + // 로컬 이미지 로드 함수 (GifticonCard와 동일) + private func loadLocalImage(from fileName: String) -> UIImage? { + guard !fileName.isEmpty else { return nil } + + let documentsPath = FileManager.default.urls(for: .documentDirectory, + in: .userDomainMask)[0] + let fileURL = documentsPath.appendingPathComponent(fileName) + + if FileManager.default.fileExists(atPath: fileURL.path) { + return UIImage(contentsOfFile: fileURL.path) + } + return nil + } + var body: some View { Form { Section(header: Text("선택 쿠폰")) { + // 이미지 표시 로직 수정 if !selectedCoupon.imagePath.isEmpty { - AsyncImage(url: URL(string: selectedCoupon.imagePath)) { image in - image + if let localImage = loadLocalImage(from: selectedCoupon.imagePath) { + Image(uiImage: localImage) .resizable() .clipShape(.rect(cornerRadius: 12)) .aspectRatio(contentMode: .fit) .padding() - } placeholder: { - Rectangle() - .fill(Color.gray.opacity(0.1)) - .frame(height: 200) - .overlay( - Text(selectedCoupon.brand) - .foregroundColor(.gray) - ) + } else { + AsyncImage(url: URL(string: selectedCoupon.imagePath)) { image in + image + .resizable() + .clipShape(.rect(cornerRadius: 12)) + .aspectRatio(contentMode: .fit) + .padding() + } placeholder: { + defaultImagePlaceholder + } } } else { Image(systemName: "gift") @@ -198,6 +215,7 @@ struct SelectedCouponCell: View { .padding() } + // 나머지 UI 코드는 동일... HStack { Text("상품명") .foregroundColor(.gray) @@ -234,6 +252,16 @@ struct SelectedCouponCell: View { } } } + + private var defaultImagePlaceholder: some View { + Rectangle() + .fill(Color.gray.opacity(0.1)) + .frame(height: 200) + .overlay( + Text(selectedCoupon.brand) + .foregroundColor(.gray) + ) + } } // 수정 가능한 기프티콘 셀 (수정 모드) @@ -302,17 +330,3 @@ struct EditableCouponCell: View { } } } - -#Preview { - // For preview purposes, create a dummy gifticon - let gifticon = Gifticon( - brand: "스타벅스", - productName: "아메리카노", - expirationDate: Date().addingTimeInterval(30*24*60*60), - isUsed: false, - imagePath: "" - ) - - return SelectedCouponView(selectedGifticon: gifticon) - .modelContainer(for: Gifticon.self, inMemory: true) -} diff --git a/CouponWallet/TrashView.swift b/CouponWallet/Views/Coupon/TrashView.swift similarity index 100% rename from CouponWallet/TrashView.swift rename to CouponWallet/Views/Coupon/TrashView.swift diff --git a/CouponWallet/SettingView.swift b/CouponWallet/Views/Setting/SettingView.swift similarity index 100% rename from CouponWallet/SettingView.swift rename to CouponWallet/Views/Setting/SettingView.swift diff --git a/CouponWallet/ThemeView.swift b/CouponWallet/Views/Theme/ThemeView.swift similarity index 100% rename from CouponWallet/ThemeView.swift rename to CouponWallet/Views/Theme/ThemeView.swift diff --git a/README.md b/README.md new file mode 100644 index 0000000..c463833 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# CouponWallet +![Swift](https://img.shields.io/badge/Swift-5.9-F05138?logo=swift) +![Platform](https://img.shields.io/badge/Platforms-iOS%2018.0+-007AFF?logo=apple) + +CouponWallet은 디지털 기프티콘을 관리하기 위한 종합적인 iOS 애플리케이션입니다. 이 앱을 통해 사용자는 디지털 쿠폰을 스캔하고, 저장하고, 정리하며, 한 곳에서 편리하게 관리할 수 있습니다. + +## 개요 + +- 프로젝트 이름: CouponWallet +- 프로젝트 기간: 3월 7일 ~ 3월 11일 +- 개발 언어: Swift +- 개발 프레임워크: SwiftUI, PhotosUI, Vision, VisionKit, SwiftData +- 멤버: 김대홍, 조영민, 홍석평 + +## 🌟 주요 기능 +### 홈 탭 +- **보유 쿠폰 보기**: 사용 가능한 유효한 쿠폰을 한눈에 볼 수 있습니다 +- **쿠폰 필터링**: 브랜드별로 쉽게 필터링 가능 (스타벅스, 치킨, CU, GS25 등) +- **쿠폰 추가**: 다음 방법으로 새 기프티콘을 추가할 수 있습니다: + - 카메라 직접 스캔(구현 중...) + - 사진 갤러리에서 가져오기 +### 사용·만료 탭 +- **사용/만료된 쿠폰 보기**: 사용 이력을 추적할 수 있습니다 +- **필터 옵션**: "사용 완료" 또는 "만료" 상태별로 정렬 +- **다중 선택**: 여러 쿠폰을 선택하여 휴지통으로 이동 +- **정렬**: 날짜별 정렬 (최신순/오래된 순) +### 설정 탭 +- **프로필 설정**: 사용자 프로필 정보 관리 +- **알림 설정**: 앱 알림 구성 +- **테마 설정**: 라이트 모드와 다크 모드 전환 +- **휴지통 관리**: 쿠폰 복구 또는 영구 삭제 +## 📱 핵심 기능 +### 쿠폰 이미지 스캐닝 +Vision 및 VisionKit 프레임워크를 사용하여: +- 기프티콘 이미지에서 텍스트를 자동으로 인식 +- 다음과 같은 주요 정보 추출: + - 브랜드명 + - 상품명 + - 유효기간 +- 카메라 스캔과 갤러리 이미지 모두 처리 + +### 직관적인 UI +- 스와이프 가능한 쿠폰 상세 정보 +- 탭 기반 네비게이션 +- 쿠폰 상태에 따른 컨텍스트 액션 + +### SwiftData를 사용한 데이터 관리 +- 쿠폰 정보의 영구 저장 +- 복원 옵션이 있는 휴지통 기능 + +## 🔧 기술적 구현 +### 사용된 프레임워크 +- **SwiftData**: 데이터 영속성 레이어 +- **Vision/VisionKit**: OCR 및 텍스트 인식 +- **PhotosUI**: 사진 라이브러리 통합 + +### 주요 구성 요소 +- **GifticonScanManager**: OCR 및 텍스트 추출 처리 +- **TextAnalyzer**: 스캔된 텍스트에서 의미 있는 데이터 파싱 및 추출 +- **Gifticon Model**: 쿠폰 정보 저장을 위한 SwiftData 모델 +- **사용자 정의 뷰**: 다양한 쿠폰 상태 및 작업을 위한 특수 뷰 + +### 사용자 경험 기능 +- 쿠폰 공유를 위한 쿠폰 이미지 공유 기능 +- 쿠폰 카드 애니메이션 전환 효과 +- 앨범에 있는 쿠폰 자동 스캔 및 이미지 추가 + +## 📷 스크린샷 +

+ 홈 화면 + 상세 화면 + 설정 화면 + 만료 화면 + 스캔 화면 +

+ +## 📝 피그마 설계도 +스크린샷 2025-03-11 오후 2 44 05 + + +## 🚀 시작하기 +### 요구 사항 +- iOS 18.0+ +- Xcode 16.0+ +- Swift 5.0+ + +## 🔮 향후 개선 사항 +- 바코드/QR 코드 스캐닝 +- 유효기간 알림 +- 인기 브랜드 API와의 통합 +- 소셜 공유 기능 +- 통계 및 사용 분석 + +## 👥 역할 + +### 김대홍 +- 쿠폰 선택 화면 +- 쿠폰 카드 슬라이드 애니메이션 +- 쿠폰 사용 완료 및 수정 로직 구현 + +### 조영민 +- 기본 틀 프로젝트 제작 +- PhotoPicker로 이미지 가져오기 +- vision 프레임워크로 쿠폰 이미지 스캔 기능 구현 + +### 홍석평 +- 쿠폰 공유 기능 +- 쿠폰 삭제 기능 +- 라이트 모드 다크 모드 구현 + +## 느낀 점과 개선할 점 + +### 김대홍 +- 사용가능 쿠폰 선택 화면: 더미 데이터에서 실행되던 코드가, 더미 데이터 없이 Merge 작업하니 에러가 발생되는 문제를 해결하는 단계에서 어려움을 겪었습니다. +- 사용완료 및 기간만료 쿠폰 선택 화면: 2개의 탭 뷰를 동시에 연결되는 화면을 만들려다 보니 어려움을 느껴, 사용가능 쿠폰과 사용완료/기간만료 쿠폰 선택을 화면을 분할하였습니다. +- 쿠폰을 좌우로 슬라이드 뷰를 구성하면서, 정렬순서를 탭 뷰의 정렬순서대로 구현하는 것이 쉽지 않았습니다. +- 화면 기준으로 업무를 분배하다보니 연결되는 화면이 있을 경우, 협업이 필요함을 알았습니다. +- Github: 처음 설정할 때, .gitignore 만들어서인지, 코드 외 충돌 문제가 없어서 원활하게 진행할 수 있지 않았나 싶습니다. + +### 조영민 +- **Vision 스캔 기능**: 이미지 스캔 처리 기능을 처음 구현해보았는데 생각보다 고려해야 할 것이 많고 이미지 인식이 잘 안 되어서 조금 더 세부적으로 공부한 다음 인식률을 올려보고 싶다는 생각이 들었습니다. +- **뷰 나누기의 중요성**: 뷰를 세부적으로 나눠야 유지보수 측면에서도 편하고 유지보수가 아니더라도 초기 개발 과정에서도 중요하다는 것을 느꼈습니다. 특히 이 부분에서 재사용성과 가독성이 크게 향상될 수 있다는 것을 느꼈습니다. +- **깃의 중요성**: 깃을 제대로 알아야 어느 부분에서 충돌이 생긴 건지 알고 빠르게 해결해야 개발 시간도 확보되고 낭비되는 시간을 줄일 수 있다는 것을 느껴서 깃을 제대로 알고 써야겠다는 생각을 했습니다. + +### 홍석평 +- **구현**: 선택한 쿠폰을 캡처하고 공유할 때 ShareLink를 활용하여 다양한 앱으로 쉽게 공유하도록 만들었습니다. 또한, 다크 모드 전환 기능을 구현하여 사용자 환경에 맞는 UI를 지원하도록 했습니다. 쿠폰 삭제의 복원 기능을 구현하는 과정에서 어려움을 겪었습니다. +- 예상치 못한 문제를 해결하는 과정에서 많은 것을 배울 수 있었으며, 이를 통해 더욱 견고한 기능을 구현하는 방법을 익히게 되었습니다. +- **Git**: 팀원들과 Git을 활용한 협업을 진행하며 부족한 부분을 깨닫게 되었습니다. +자주 사용하면서 익숙해지기 위해 학습을 지속하고 있으며, 실전 경험을 통해 보다 효과적인 Git 활용법을 익히고 있습니다.