diff --git a/.gitignore b/.gitignore index 2ab5675..baf1fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ Packages/ # Environment variables .env + +# Xcode user state +*.xcuserstate +xcuserdata/ + +baseURL.swift \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..48b77b0 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# ๐Ÿ“ฑ PicPick + +> ๋ณต์žกํ•œ ๊ฐ€๊ฒฉํ‘œ๋ฅผ ์Šค์บ” 1๋ฒˆ์œผ๋กœ '๊ฐ€์„ฑ๋น„ ํ™•์‹ '๊ณผ '์ตœ์  ํƒ€์ด๋ฐ'์œผ๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ์‡ผํ•‘์˜ ์–ด์‹œ์Šคํ„ดํŠธ + +> ๋งˆํŠธ ๊ฐ€๊ฒฉํ‘œ์˜ ๊ฐ€๊ณต๋˜์ง€ ์•Š์€ '๋กœ์šฐ ๋ฐ์ดํ„ฐ'๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ‰๊ฐ ํ–‰๋™ํ•  ์ˆ˜ ์žˆ๋Š” '์˜์‚ฌ๊ฒฐ์ • ์ •๋ณด'๋กœ ์ „ํ™˜ํ•˜๋Š” ์‡ผํ•‘ ๋ณด์กฐ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + +--- + +## ๐Ÿงฉ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +- **ํ”„๋กœ์ ํŠธ๋ช…**: PicPick +- **ํ”Œ๋žซํผ**: iOS +- **๊ฐœ๋ฐœ ์ธ์›**: iOS 2๋ช… / Backend 1๋ช… / Design 1๋ช… / PM 1๋ช… +- **ํƒ€๊ฒŸ ์‚ฌ์šฉ์ž**: ์‹ค์šฉ ์ค‘์‹ฌ์˜ ๊ตฌ๋งค๋ฅผ ์ถ”๊ตฌํ•˜๋Š” ์˜คํ”„๋ผ์ธ ์†Œ๋น„์ž +--- + +## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ + +- OCR ๊ธฐ๋ฐ˜ ์ œํ’ˆ ๋ถ„์„ +- ๋„ค์ด๋ฒ„ API๋ฅผ ํ†ตํ•œ ์˜จ์˜คํ”„๋ผ์ธ ๊ฐ€๊ฒฉ ๋น„๊ต +- AI ๊ฐ€์„ฑ๋น„ ๋ถ„์„ +- ์˜คํ”„๋ผ์ธ ๋งˆํŠธ ํ–‰์‚ฌ์ •๋ณด ์ œ๊ณต + +--- + +## ๐Ÿ›  ๊ธฐ์ˆ  ์Šคํƒ + +### iOS +- SwiftUI +- UIKit +- Combine +- MapKit +- CoreLocation + +### Tools +- Xcode +- Git / GitHub +- Figma +- Notion + +--- + + +## ๐Ÿ”„ ํ™”๋ฉด ํ๋ฆ„ + +1. ์˜จ๋ณด๋”ฉ +2. ๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ +3. ์œ„์น˜ ์„ค์ • +4. OCR ๊ธฐ๋Šฅ ์‹คํ–‰ +5. OCR ๊ฒฐ๊ณผ ํ™”๋ฉด +6. ๊ฐ€์„ฑ๋น„ ์„ธ๋ถ€์ •๋ณด ํ™”๋ฉด + +--- + +## ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท + + +### 1) ์˜จ๋ณด๋”ฉ +์•ฑ์— ๋“ค์–ด๊ฐ€๋Š” ์ฒซ ํ™”๋ฉด์ž…๋‹ˆ๋‹ค. + +Onboarding + + +--- + +### 2) ๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ +UUID๋ฅผ ํ†ตํ•ด ๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +IMG_9013 + + +--- + +### 3) ์œ„์น˜ ์„ค์ • +ํ˜„์žฌ ์œ„์น˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฃผ๋ณ€ ๋งˆํŠธ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. + +Map + + +--- + +### 4) OCR ๊ธฐ๋Šฅ ์‹คํ–‰ +๊ฐ€๊ฒฉํ‘œ๋ฅผ ์ดฌ์˜ํ•˜์—ฌ ์ƒํ’ˆ ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค. + +1) OCR ํ™”๋ฉด ์‹คํ–‰ ์งํ›„ +OCRScan1 + +2) OCR ์Šค์บ” ํ™”๋ฉด +OCRScan2 + +--- + +### 5) OCR ๊ฒฐ๊ณผ ํ™”๋ฉด +์˜จยท์˜คํ”„๋ผ์ธ ๊ฐ€๊ฒฉ์„ ๋น„๊ตํ•˜๊ณ , 1ํšŒ ์‚ฌ์šฉ ๋‹น ๊ฐ€๊ฒฉ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +OCRResult + + +--- + +### 6) ๊ฐ€์„ฑ๋น„ ์„ธ๋ถ€์ •๋ณด ํ™”๋ฉด +AI๋ฅผ ํ†ตํ•ด ๊ฐ€์„ฑ๋น„ ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๊ณ , ๊ตฌ๋งค ์ถ”์ฒœ ๋ฐ ํ’ˆ์งˆ๊ณผ ๊ฐ€๊ฒฉ ์š”์•ฝ, ํ•ด๋‹น ์ œํ’ˆ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + +DetailTop + +DetailMiddle + +DetailBottom + +--- + +## ๐Ÿ‘ฉโ€๐Ÿ’ป ๊ฐœ๋ฐœ์ž + +| ์ด๋ฆ„ | ์—ญํ•  | +|----|----| +| ์†์ฑ„์› | iOS ๊ฐœ๋ฐœ | +| ๋ฐ•์ง„์ˆ˜ | iOS ๊ฐœ๋ฐœ | diff --git a/Shoppingmate-Frontend-Info.plist b/Shoppingmate-Frontend-Info.plist new file mode 100644 index 0000000..909be87 --- /dev/null +++ b/Shoppingmate-Frontend-Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleGetInfoString + + UIAppFonts + + Pretendard-Regular.otf +Pretendard-Bold.otf + Pretendard-Bold.otf + + + diff --git a/Shoppingmate_Frontend.xcodeproj/project.pbxproj b/Shoppingmate_Frontend.xcodeproj/project.pbxproj index 83ab286..af65977 100644 --- a/Shoppingmate_Frontend.xcodeproj/project.pbxproj +++ b/Shoppingmate_Frontend.xcodeproj/project.pbxproj @@ -255,11 +255,14 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 73GWX5Y7JS; + DEVELOPMENT_TEAM = 56MQCL972R; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Shoppingmate-Frontend-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_NSCameraUsageDescription = "์‚ฌ์ง„ ์ดฌ์˜์„ ์œ„ํ•ด ์นด๋ฉ”๋ผ ์ ‘๊ทผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "๊ฐ€๊ฒฉํ‘œ๋ฅผ ์ดฌ์˜ํ•œ ์œ„์น˜๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "์‚ฌ์šฉ์ž ์œ„์น˜๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -271,7 +274,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.chaewon.Shoppingmate.Frontend; + PRODUCT_BUNDLE_IDENTIFIER = "com.jinsoo.Shoppingmate-Frontend"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -290,11 +293,14 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 73GWX5Y7JS; + DEVELOPMENT_TEAM = 56MQCL972R; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Shoppingmate-Frontend-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_NSCameraUsageDescription = "์‚ฌ์ง„ ์ดฌ์˜์„ ์œ„ํ•ด ์นด๋ฉ”๋ผ ์ ‘๊ทผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.\n"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "๊ฐ€๊ฒฉํ‘œ๋ฅผ ์ดฌ์˜ํ•œ ์œ„์น˜๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "์‚ฌ์šฉ์ž ์œ„์น˜๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -306,7 +312,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.chaewon.Shoppingmate.Frontend; + PRODUCT_BUNDLE_IDENTIFIER = "com.jinsoo.Shoppingmate-Frontend"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; diff --git a/Shoppingmate_Frontend.xcodeproj/project.xcworkspace/xcuserdata/chaewon.xcuserdatad/UserInterfaceState.xcuserstate b/Shoppingmate_Frontend.xcodeproj/project.xcworkspace/xcuserdata/chaewon.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 0a67888..0000000 Binary files a/Shoppingmate_Frontend.xcodeproj/project.xcworkspace/xcuserdata/chaewon.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Check.png b/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Check.png new file mode 100644 index 0000000..7783d04 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Check.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Contents.json new file mode 100644 index 0000000..0890b5c --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/Check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Check.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/Contents.json new file mode 100644 index 0000000..3bae821 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "LegendDelete.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/LegendDelete.png b/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/LegendDelete.png new file mode 100644 index 0000000..54ad3b2 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/LegendDelete.imageset/LegendDelete.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/Contents.json new file mode 100644 index 0000000..91471fa --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "PICPICK.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/PICPICK.png b/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/PICPICK.png new file mode 100644 index 0000000..a0a7cb4 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/PICPICK.imageset/PICPICK.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Contents.json new file mode 100644 index 0000000..90d7ca1 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Pin.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Pin.svg b/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Pin.svg new file mode 100644 index 0000000..f3941c1 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/Pin.imageset/Pin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/Contents.json new file mode 100644 index 0000000..9449b13 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "bArrow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/bArrow.png b/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/bArrow.png new file mode 100644 index 0000000..c383e84 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/bArrow.imageset/bArrow.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/Contents.json new file mode 100644 index 0000000..efd3cbe --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "backArrow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/backArrow.png b/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/backArrow.png new file mode 100644 index 0000000..ae1ef62 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/backArrow.imageset/backArrow.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/Contents.json new file mode 100644 index 0000000..c397fd5 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "bubble.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/bubble.png b/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/bubble.png new file mode 100644 index 0000000..61b8fae Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/bubble.imageset/bubble.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/Contents.json new file mode 100644 index 0000000..bc3a886 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cameraBack.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/cameraBack.png b/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/cameraBack.png new file mode 100644 index 0000000..4f78595 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/cameraBack.imageset/cameraBack.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/Contents.json new file mode 100644 index 0000000..5b49284 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "currentLocation.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/currentLocation.svg b/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/currentLocation.svg new file mode 100644 index 0000000..50199ad --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/currentLocation.imageset/currentLocation.svg @@ -0,0 +1,3 @@ + + + diff --git a/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/Contents.json new file mode 100644 index 0000000..2aa7c8d --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "goMap.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/goMap.png b/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/goMap.png new file mode 100644 index 0000000..f590b01 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/goMap.imageset/goMap.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/Contents.json new file mode 100644 index 0000000..af5fe45 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_beauty_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/icon_beauty_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/icon_beauty_1.png new file mode 100644 index 0000000..3eaad5c Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_1.imageset/icon_beauty_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/Contents.json new file mode 100644 index 0000000..30d5c64 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_beauty_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/icon_beauty_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/icon_beauty_2.png new file mode 100644 index 0000000..d49ba6f Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_2.imageset/icon_beauty_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/Contents.json new file mode 100644 index 0000000..15de7cd --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_beauty_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/icon_beauty_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/icon_beauty_3.png new file mode 100644 index 0000000..1ecb827 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_3.imageset/icon_beauty_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/Contents.json new file mode 100644 index 0000000..87d1547 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_beauty_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/icon_beauty_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/icon_beauty_4.png new file mode 100644 index 0000000..e8d34de Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_4.imageset/icon_beauty_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/Contents.json new file mode 100644 index 0000000..5e4f731 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_beauty_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/icon_beauty_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/icon_beauty_5.png new file mode 100644 index 0000000..a8fa440 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_beauty_5.imageset/icon_beauty_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/Contents.json new file mode 100644 index 0000000..8b3098c --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_fresh_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/icon_fresh_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/icon_fresh_1.png new file mode 100644 index 0000000..5f46793 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_1.imageset/icon_fresh_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/Contents.json new file mode 100644 index 0000000..6c042ef --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_fresh_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/icon_fresh_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/icon_fresh_2.png new file mode 100644 index 0000000..4460984 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_2.imageset/icon_fresh_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/Contents.json new file mode 100644 index 0000000..a927852 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_fresh_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/icon_fresh_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/icon_fresh_3.png new file mode 100644 index 0000000..a75688b Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_3.imageset/icon_fresh_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/Contents.json new file mode 100644 index 0000000..1554541 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_fresh_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/icon_fresh_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/icon_fresh_4.png new file mode 100644 index 0000000..ae2798a Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_4.imageset/icon_fresh_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/Contents.json new file mode 100644 index 0000000..c94e483 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_fresh_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/icon_fresh_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/icon_fresh_5.png new file mode 100644 index 0000000..c8eff06 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_fresh_5.imageset/icon_fresh_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/Contents.json new file mode 100644 index 0000000..e2c7528 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_hygiene_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/icon_hygiene_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/icon_hygiene_1.png new file mode 100644 index 0000000..1c1d3df Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_1.imageset/icon_hygiene_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/Contents.json new file mode 100644 index 0000000..7c1e3d6 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_hygiene_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/icon_hygiene_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/icon_hygiene_2.png new file mode 100644 index 0000000..c45882b Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_2.imageset/icon_hygiene_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/Contents.json new file mode 100644 index 0000000..1b91779 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_hygiene_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/icon_hygiene_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/icon_hygiene_3.png new file mode 100644 index 0000000..460e885 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_3.imageset/icon_hygiene_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/Contents.json new file mode 100644 index 0000000..e9662e4 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_hygiene_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/icon_hygiene_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/icon_hygiene_4.png new file mode 100644 index 0000000..c15fbdb Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_4.imageset/icon_hygiene_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/Contents.json new file mode 100644 index 0000000..4b3865f --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_hygiene_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/icon_hygiene_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/icon_hygiene_5.png new file mode 100644 index 0000000..5d4be34 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_hygiene_5.imageset/icon_hygiene_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/Contents.json new file mode 100644 index 0000000..98f57ec --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_living_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/icon_living_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/icon_living_1.png new file mode 100644 index 0000000..86e5ea3 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_living_1.imageset/icon_living_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/Contents.json new file mode 100644 index 0000000..1f34f1d --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_living_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/icon_living_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/icon_living_2.png new file mode 100644 index 0000000..3bae652 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_living_2.imageset/icon_living_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/Contents.json new file mode 100644 index 0000000..bdc4f8b --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_living_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/icon_living_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/icon_living_3.png new file mode 100644 index 0000000..11d1296 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_living_3.imageset/icon_living_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/Contents.json new file mode 100644 index 0000000..53e87da --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_living_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/icon_living_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/icon_living_4.png new file mode 100644 index 0000000..2fa6549 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_living_4.imageset/icon_living_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/Contents.json new file mode 100644 index 0000000..7d8bc5e --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_living_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/icon_living_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/icon_living_5.png new file mode 100644 index 0000000..7239796 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_living_5.imageset/icon_living_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/Contents.json new file mode 100644 index 0000000..f70a584 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_pet_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/icon_pet_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/icon_pet_1.png new file mode 100644 index 0000000..c787a72 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_1.imageset/icon_pet_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/Contents.json new file mode 100644 index 0000000..e9e15f6 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_pet_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/icon_pet_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/icon_pet_2.png new file mode 100644 index 0000000..9e609e1 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_2.imageset/icon_pet_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/Contents.json new file mode 100644 index 0000000..75c5688 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_pet_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/icon_pet_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/icon_pet_3.png new file mode 100644 index 0000000..cd4c7f4 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_3.imageset/icon_pet_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/Contents.json new file mode 100644 index 0000000..0b10833 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_pet_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/icon_pet_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/icon_pet_4.png new file mode 100644 index 0000000..8bbb553 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_4.imageset/icon_pet_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/Contents.json new file mode 100644 index 0000000..6a82b8a --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_pet_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/icon_pet_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/icon_pet_5.png new file mode 100644 index 0000000..ba373fd Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_pet_5.imageset/icon_pet_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/Contents.json new file mode 100644 index 0000000..83a5c10 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_preference_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/icon_preference_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/icon_preference_1.png new file mode 100644 index 0000000..1237ef5 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_1.imageset/icon_preference_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/Contents.json new file mode 100644 index 0000000..0fef2c9 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_preference_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/icon_preference_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/icon_preference_2.png new file mode 100644 index 0000000..b77b974 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_2.imageset/icon_preference_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/Contents.json new file mode 100644 index 0000000..33c2ecb --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_preference_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/icon_preference_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/icon_preference_3.png new file mode 100644 index 0000000..bd96e05 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_3.imageset/icon_preference_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/Contents.json new file mode 100644 index 0000000..ad86947 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_preference_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/icon_preference_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/icon_preference_4.png new file mode 100644 index 0000000..a7de030 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_4.imageset/icon_preference_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/Contents.json new file mode 100644 index 0000000..97eafec --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_preference_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/icon_preference_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/icon_preference_5.png new file mode 100644 index 0000000..796c9dc Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_preference_5.imageset/icon_preference_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/Contents.json new file mode 100644 index 0000000..18b1487 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/icon_processed_1.png b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/icon_processed_1.png new file mode 100644 index 0000000..7bc082c Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_1.imageset/icon_processed_1.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/Contents.json new file mode 100644 index 0000000..fcb81cb --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/icon_processed_2.png b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/icon_processed_2.png new file mode 100644 index 0000000..1d52e1e Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_2.imageset/icon_processed_2.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/Contents.json new file mode 100644 index 0000000..4aadba7 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/icon_processed_3.png b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/icon_processed_3.png new file mode 100644 index 0000000..a2ea1ba Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_3.imageset/icon_processed_3.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/Contents.json new file mode 100644 index 0000000..c3bb104 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/icon_processed_4.png b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/icon_processed_4.png new file mode 100644 index 0000000..4460874 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_4.imageset/icon_processed_4.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents 2.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents 2.json new file mode 100644 index 0000000..3ba40e3 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents 2.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents.json new file mode 100644 index 0000000..3ba40e3 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_processed_5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/icon_processed_5.png b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/icon_processed_5.png new file mode 100644 index 0000000..131353e Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/icon_processed_5.imageset/icon_processed_5.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/Contents.json new file mode 100644 index 0000000..ad70463 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "image 12.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/image 12.png b/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/image 12.png new file mode 100644 index 0000000..6f29cff Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/image 12.imageset/image 12.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/Contents.json new file mode 100644 index 0000000..bf6f78b --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "jh.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/jh.png b/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/jh.png new file mode 100644 index 0000000..2f3eb3b Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/jh.imageset/jh.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/Contents.json new file mode 100644 index 0000000..0012f1b --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "mapPin.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/mapPin.svg b/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/mapPin.svg new file mode 100644 index 0000000..7b066d1 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/mapPin.imageset/mapPin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/Contents.json new file mode 100644 index 0000000..8896e6d --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "picpickLogo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/picpickLogo.png b/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/picpickLogo.png new file mode 100644 index 0000000..c13a445 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/picpickLogo.imageset/picpickLogo.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/Contents.json new file mode 100644 index 0000000..f393f51 --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "priceIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/priceIcon.png b/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/priceIcon.png new file mode 100644 index 0000000..6a420dd Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/priceIcon.imageset/priceIcon.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/Contents.json new file mode 100644 index 0000000..7b018dd --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pricetag.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/pricetag.png b/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/pricetag.png new file mode 100644 index 0000000..49d53d8 Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/pricetag.imageset/pricetag.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/Contents.json new file mode 100644 index 0000000..3fd35be --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "purpleBar.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/purpleBar.png b/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/purpleBar.png new file mode 100644 index 0000000..9214f6e Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/purpleBar.imageset/purpleBar.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/Contents.json new file mode 100644 index 0000000..3fa074d --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "qualityIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/qualityIcon.png b/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/qualityIcon.png new file mode 100644 index 0000000..2f267af Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/qualityIcon.imageset/qualityIcon.png differ diff --git a/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/Contents.json b/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/Contents.json new file mode 100644 index 0000000..dafaa7b --- /dev/null +++ b/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "sparkles.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/sparkles.png b/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/sparkles.png new file mode 100644 index 0000000..5442d0e Binary files /dev/null and b/Shoppingmate_Frontend/Assets.xcassets/sparkles.imageset/sparkles.png differ diff --git a/Shoppingmate_Frontend/Model/AnalysisIconProvider.swift b/Shoppingmate_Frontend/Model/AnalysisIconProvider.swift new file mode 100644 index 0000000..24022fb --- /dev/null +++ b/Shoppingmate_Frontend/Model/AnalysisIconProvider.swift @@ -0,0 +1,107 @@ +// +// AnalysisIconProvider.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/9/26. +// + +import SwiftUI +import Foundation + +enum AnalysisCategory: String { + case ์‹ ์„  + case ๊ฐ€๊ณต + case ๊ธฐํ˜ธ + case ์œ„์ƒ + case ๋ทฐํ‹ฐ + case ๋ฆฌ๋น™ + case ํŽซ๋ผ์ดํ”„ + + static func from(serverValue: String) -> AnalysisCategory? { + let trimmed = serverValue.trimmingCharacters(in: .whitespacesAndNewlines) + + // ์„œ๋ฒ„ ํ‘œํ˜„์ด ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋Š” ์ผ€์ด์Šค ํก์ˆ˜ + switch trimmed { + case "ํŽซ/๋ผ์ดํ”„", "ํŽซ ๋ผ์ดํ”„": + return .ํŽซ๋ผ์ดํ”„ + default: + return AnalysisCategory(rawValue: trimmed) + } + } +} + +struct AnalysisIconProvider { + + static func icons(for categoryString: String) -> [String] { + + guard let category = AnalysisCategory.from(serverValue: categoryString) else { + return Array(repeating: "icon_default", count: 5) + } + + switch category { + + case .์‹ ์„ : + return [ + "icon_fresh_1", + "icon_fresh_2", + "icon_fresh_3", + "icon_fresh_4", + "icon_fresh_5" + ] + + case .๊ฐ€๊ณต: + return [ + "icon_processed_1", + "icon_processed_2", + "icon_processed_3", + "icon_processed_4", + "icon_processed_5" + ] + + case .๊ธฐํ˜ธ: + return [ + "icon_preference_1", + "icon_preference_2", + "icon_preference_3", + "icon_preference_4", + "icon_preference_5" + ] + + case .์œ„์ƒ: + return [ + "icon_hygiene_1", + "icon_hygiene_2", + "icon_hygiene_3", + "icon_hygiene_4", + "icon_hygiene_5" + ] + + case .๋ทฐํ‹ฐ: + return [ + "icon_beauty_1", + "icon_beauty_2", + "icon_beauty_3", + "icon_beauty_4", + "icon_beauty_5" + ] + + case .๋ฆฌ๋น™: + return [ + "icon_living_1", + "icon_living_2", + "icon_living_3", + "icon_living_4", + "icon_living_5" + ] + + case .ํŽซ๋ผ์ดํ”„: + return [ + "icon_pet_1", + "icon_pet_2", + "icon_pet_3", + "icon_pet_4", + "icon_pet_5" + ] + } + } +} diff --git a/Shoppingmate_Frontend/Model/DTO.swift b/Shoppingmate_Frontend/Model/DTO.swift new file mode 100644 index 0000000..1679b88 --- /dev/null +++ b/Shoppingmate_Frontend/Model/DTO.swift @@ -0,0 +1,68 @@ +// +// LocationDTO.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 12/29/25. +// + +import CoreLocation +import Foundation + +//DTO ํŒŒ์ผ์—๋Š” struct/extension๋งŒ ์žˆ์–ด์•ผ ํ•จ + +struct UUIDDTO: Codable { + let uuid: String +} + +struct UserIdResponse: Codable { + let userId: Int +} + +// DTO: Data Transfer Object +struct LocationDTO: Codable { + let userId: Int + let latitude: Double + let longitude: Double +} + +struct ScanUploadRequest: Codable { //scan post + let userId: Int + let items: [ScanUploadItem] +} + +struct ScanUploadItem: Codable { //scan post + let scanName: String + let scanPrice: Int +} + +struct ScanItemResponse: Codable, Identifiable { //scan get + let userId: Int + let scanId: Int + let scanName: String + let scanPrice: Int + let naverPrice: Int? + let naverBrand: String? + let naverMaker: String? + let naverImage: String? + let aiUnitPrice: String? + let isShown: Bool + + var id: Int { scanId } +} + +struct scanIdDTO: Codable { + let scanId: Int +} + + +// Apple์ด ๋งŒ๋“  CLLocation type์— ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +// CLLocation ์ƒ์†์€ ๋ถˆ๊ฐ€, ํ™•์žฅ๋งŒ ๊ฐ€๋Šฅ +extension CLLocation { + func toDTO(userId: Int) -> LocationDTO {// CLLocation -> LocationDTO๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ + LocationDTO( + userId: userId, + latitude: self.coordinate.latitude,// ํ˜„์žฌ ์œ„์น˜ ๊ฐ์ฒด์˜ ์œ„๋„ ๊ฐ’์„ ๊บผ๋‚ด์„œ DTO์— ๋„ฃ๋Š”๋‹ค + longitude: self.coordinate.longitude + ) + } +} diff --git a/Shoppingmate_Frontend/Model/DetailData.swift b/Shoppingmate_Frontend/Model/DetailData.swift new file mode 100644 index 0000000..75cb2f7 --- /dev/null +++ b/Shoppingmate_Frontend/Model/DetailData.swift @@ -0,0 +1,102 @@ +// +// DetailMock.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/6/26. +// + +import SwiftUI + +struct AnalysisIndex: Codable, Identifiable { + let id = UUID() + let name: String + let reason: String +} + +struct DetailResponse: Codable { + let naverImage: String? + let naverBrand: String? + let scanName: String + let category: String + + let pickScore: Double + let reliabilityScore: Double + + let scanPrice: Double + let naverPrice: Double + let priceDiff: Double + let isCheaper: Bool + + let aiUnitPrice: String? + + let indexes: [AnalysisIndex] + + let qualityState: Bool + let qualitySummary: String + let priceState: Bool + let priceSummary: String + let conclusion: String +} + +//struct HardcodedSavingInfo { +// let savingAmount: Int +//} +// +//let hardcodedSavingMap: [String: HardcodedSavingInfo] = [ +// "์ƒคํ”„๋ž€ 2.6L ๋ฆด๋ ‰์‹ฑ ์•„๋กœ๋งˆ(๋ฆฌํ•„)": HardcodedSavingInfo(savingAmount: 610), +// "์˜ค๋šœ๊ธฐ ์”ป์–ด๋‚˜์˜จ ์˜ค๋šœ๊ธฐ ์Œ€ 1kg": HardcodedSavingInfo(savingAmount: 1290), +// "๋งฅ์Šค์›ฐ ์ปคํ”ผ๋ฏน์Šค ์˜ค๋ฆฌ์ง€๋„ 50T": HardcodedSavingInfo(savingAmount: 890), +// "์˜ค๋šœ๊ธฐ ์ง„๋ผ๋ฉด ๋งค์šด๋ง› 5์ž…": HardcodedSavingInfo(savingAmount: 1060), +// "๋ถ€์ž๋˜๋Š” ์ง‘ ๋‚ด์ถ”๋Ÿด ํด๋ž˜์‹ ํ™”์žฅ์ง€ 30๋กค": HardcodedSavingInfo(savingAmount: 1600), +// "ํ”ผ์ฃค ์„ฌ์œ ์œ ์—ฐ์ œ ํผํ”Œ๋ผ๋ฒค๋” 1600ml ๋ฆฌํ•„": HardcodedSavingInfo(savingAmount: 310), +// "์ŠคํŒŒํด ์ƒ์ˆ˜ ๋ฌด๋ผ๋ฒจ, 500ml 20๊ฐœ": HardcodedSavingInfo(savingAmount: 800) +//] +// +//extension DetailResponse { +// +// var hardcodedSavingText: String? { +// for (key, value) in hardcodedSavingMap { +// if scanName.contains(key) { +// return "\(value.savingAmount)์› ๋” ์ด๋“" +// } +// } +// return nil +// } +// +// /// ๋ทฐ์—์„œ ์ตœ์ข…์œผ๋กœ ์“ฐ๋Š” ๊ฐ’ +// var savingText: String { +// if let hardcoded = hardcodedSavingText { +// return hardcoded +// } +// +// // fallback (๋‚˜์ค‘์— ์„œ๋ฒ„ ๋ถ™์„ ๋•Œ ๋Œ€๋น„) +// return isCheaper +// ? "\(Int(naverPrice - scanPrice))์› ๋” ์ด๋“" +// : "\(Int(scanPrice - naverPrice))์› ๋” ์ด๋“" +// } +//} + +struct GeminiDetailResponse: Codable { + let pickScore: Double + let reliabilityScore: Double + + let aiUnitPrice: String? + let indexes: [AnalysisIndex] + + let qualityState: Bool + let qualitySummary: String + let priceState: Bool + let priceSummary: String + let conclusion: String +} + + +//extension AnalysisIndex { +// static let modeling: [AnalysisIndex] = [ +// AnalysisIndex(name: "Hana", reason: "WINGOํ†ต์žฅ"), +// AnalysisIndex(name: "bank2", reason: "ํ† ์Šค๋ฑ…ํฌํ†ต์žฅ"), +// AnalysisIndex(name: "bank3", reason: "ํ† ์Šค๋ฑ…ํฌ์— ์Œ“์ธ ์ด์ž"), +// AnalysisIndex(name: "bank4", reason: "MY์ž…์ถœ๊ธˆํ†ต์žฅ"), +// AnalysisIndex(name: "bank5", reason: "KB๋‚˜๋ผ์‚ฌ๋ž‘์šฐ๋Œ€ํ†ต์žฅ"), +// ] +//} diff --git a/Shoppingmate_Frontend/Model/LocationDTO.swift b/Shoppingmate_Frontend/Model/LocationDTO.swift deleted file mode 100644 index 161334a..0000000 --- a/Shoppingmate_Frontend/Model/LocationDTO.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// LocationDTO.swift -// Shoppingmate_Frontend -// -// Created by ์†์ฑ„์› on 12/29/25. -// - -import CoreLocation - -// DTO: Data Transfer Object -struct LocationDTO: Codable { - let latitude: Double - let longitude: Double -} - -// Apple์ด ๋งŒ๋“  CLLocation type์— ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ -// CLLocation ์ƒ์†์€ ๋ถˆ๊ฐ€, ํ™•์žฅ๋งŒ ๊ฐ€๋Šฅ -extension CLLocation { - func toDTO() -> LocationDTO {// CLLocation -> LocationDTO๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ - LocationDTO( - latitude: self.coordinate.latitude,// ํ˜„์žฌ ์œ„์น˜ ๊ฐ์ฒด์˜ ์œ„๋„ ๊ฐ’์„ ๊บผ๋‚ด์„œ DTO์— ๋„ฃ๋Š”๋‹ค - longitude: self.coordinate.longitude - ) - } -} diff --git a/Shoppingmate_Frontend/Model/LocationInfo.swift b/Shoppingmate_Frontend/Model/LocationInfo.swift new file mode 100644 index 0000000..8aed610 --- /dev/null +++ b/Shoppingmate_Frontend/Model/LocationInfo.swift @@ -0,0 +1,14 @@ +// +// LocationInfo.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/5/26. +// + +import CoreLocation + +//์ตœ์ข…์„ ํƒ๋œ ์œ„์น˜์ •๋ณด +struct LocationInfo { + let coordinate: CLLocationCoordinate2D//์ง€๋„์—์„œ ํ™•์ •๋œ ์ขŒํ‘œ + let address: String//Reverse Geocoding์œผ๋กœ ์–ป์€ ์ฃผ์†Œ +} diff --git a/Shoppingmate_Frontend/Model/OCRFilter.swift b/Shoppingmate_Frontend/Model/OCRFilter.swift new file mode 100644 index 0000000..e701f09 --- /dev/null +++ b/Shoppingmate_Frontend/Model/OCRFilter.swift @@ -0,0 +1,15 @@ +// +// OCRText.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/7/26. +// + +import Foundation + +struct OCRFilter: Identifiable, Codable { + let id = UUID() + let name: String + let price: Int + let rawText: String +} diff --git a/Shoppingmate_Frontend/Model/RecognizedProduct.swift b/Shoppingmate_Frontend/Model/RecognizedProduct.swift new file mode 100644 index 0000000..c609a24 --- /dev/null +++ b/Shoppingmate_Frontend/Model/RecognizedProduct.swift @@ -0,0 +1,49 @@ +// +// RecognizedProduct.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/1/26. +// + +// +import UIKit + +struct RecognizedProduct: Identifiable { + let id : UUID + let image: UIImage? //๋„ค์ด๋ฒ„ API ์ƒํ’ˆ์ด๋ฏธ์ง€ + let badge: String? //"Best ๊ฐ€์„ฑ๋น„" + let brand: String //"ํ”ผ์ฃค" + let name: String //"ํ”ผ์ฃค ์‹ค๋‚ด๊ฑด์กฐ ์„ฌ์œ ์œ ์—ฐ์ œ ๋ผ๋ฒค๋”ํ–ฅ" + let amount: String //"2.5L" + let price: String //"12,800์›" + let onlinePrice: String + let perUse: String //"1ํšŒ๋‹น 40์›" + let imageURL: String? //๋„ค์ด๋ฒ„ ์ด๋ฏธ์ง€ URL + let scanId: Int + + init( //๊ตฌ์กฐ์ฒด๊ฐ€ ๊ทธ๋ ค์งˆ ๋•Œ ํ•œ ๋ฒˆ๋งŒ ์ €์žฅ + id: UUID = UUID(), + image: UIImage? = nil, + badge: String? = nil, + brand: String, + name: String, + amount: String, + price: String, + onlinePrice: String, + perUse: String, + imageURL: String? = nil, + scanId: Int + ) { + self.id = id + self.image = image + self.badge = badge + self.brand = brand + self.name = name + self.amount = amount + self.price = price + self.onlinePrice = onlinePrice + self.perUse = perUse + self.imageURL = imageURL + self.scanId = scanId + } +} diff --git a/Shoppingmate_Frontend/Pretendard-Bold.otf b/Shoppingmate_Frontend/Pretendard-Bold.otf new file mode 100644 index 0000000..8e5e30a Binary files /dev/null and b/Shoppingmate_Frontend/Pretendard-Bold.otf differ diff --git a/Shoppingmate_Frontend/Pretendard-Regular.otf b/Shoppingmate_Frontend/Pretendard-Regular.otf new file mode 100644 index 0000000..08bf4cf Binary files /dev/null and b/Shoppingmate_Frontend/Pretendard-Regular.otf differ diff --git a/Shoppingmate_Frontend/Service/LocationService.swift b/Shoppingmate_Frontend/Service/LocationService.swift index f6ada34..5a0c013 100644 --- a/Shoppingmate_Frontend/Service/LocationService.swift +++ b/Shoppingmate_Frontend/Service/LocationService.swift @@ -32,6 +32,7 @@ final class LocationService: NSObject, ObservableObject { // super.init(): NSObject ์ดˆ๊ธฐํ™”(์ƒ์œ„ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”) ๋ฐ˜๋“œ์‹œ ํ˜ธ์ถœ super.init() + // delegate ์—ฐ๊ฒฐ: ์œ„์น˜ ์—…๋ฐ์ดํŠธ/๊ถŒํ•œ ๋ณ€๊ฒฝ ๊ฐ™์€ ์ด๋ฒคํŠธ๋ฅผ ์ด ํด๋ž˜์Šค๊ฐ€ ๋ฐ›๋„๋ก ์„ค์ • locationManager.delegate = self @@ -40,67 +41,87 @@ final class LocationService: NSObject, ObservableObject { // HundredMeters: ๋Œ€๋žต 100m ๋‹จ์œ„(๊ฐ€๊ฒฉํ‘œ/๋งค์žฅ ๋‹จ์œ„ ๊ธฐ๋ก์ด๋ฉด ๋ณดํ†ต ์ถฉ๋ถ„) locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters } - - // MARK: - Permission (๊ถŒํ•œ ์š”์ฒญ ๊ด€๋ จ) - - // requestPermission(): "์•ฑ ์‚ฌ์šฉ ์ค‘ ์œ„์น˜ ๊ถŒํ•œ" ํŒ์—…์„ ๋„์šฐ๋Š” ์š”์ฒญ - // (Info.plist์— NSLocationWhenInUseUsageDescription ์—†์œผ๋ฉด ์•ฑ ํฌ๋ž˜์‹œ) - func requestPermission() { - locationManager.requestWhenInUseAuthorization() - } - - // MARK: - Location (์œ„์น˜ ์—…๋ฐ์ดํŠธ ๊ด€๋ จ) - - // start(): ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ์ž‘ - // (์œ„์น˜๊ฐ€ ๊ฐฑ์‹ ๋˜๋ฉด delegate์˜ didUpdateLocations๊ฐ€ ํ˜ธ์ถœ๋จ) - func start() { - locationManager.startUpdatingLocation() - } - - // stop(): ์œ„์น˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์ค‘์ง€(๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ) - // "ํ•œ ๋ฒˆ๋งŒ ์ขŒํ‘œ ํ•„์š”"ํ•  ๋•Œ ๊ผญ stop ํ•ด์ฃผ๋Š” ๊ฒŒ ์ข‹์•„ - func stop() { - locationManager.stopUpdatingLocation() + + func requestCurrentLocation() { + switch authorizationStatus { + case .authorizedWhenInUse, .authorizedAlways: + print("๐Ÿ“ [BUTTON] ์œ„์น˜ ์š”์ฒญ ์‹คํ–‰") + locationManager.requestLocation() + + case .notDetermined: + print("๐Ÿ”‘ [BUTTON] ์œ„์น˜ ๊ถŒํ•œ ์š”์ฒญ") + locationManager.requestWhenInUseAuthorization() + + default: + print("โŒ ์œ„์น˜ ๊ถŒํ•œ ๊ฑฐ๋ถ€๋จ") + } } } // extension์œผ๋กœ delegate ๊ตฌํ˜„์„ ๋ถ„๋ฆฌํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์ง extension LocationService: CLLocationManagerDelegate { - + // didUpdateLocations: ์œ„์น˜๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค ์‹œ์Šคํ…œ์ด ํ˜ธ์ถœํ•ด์ฃผ๋Š” ์ฝœ๋ฐฑ(ํ•ต์‹ฌ) func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { - // locations: ์ง€๊ธˆ๊นŒ์ง€ ๋“ค์–ด์˜จ ์œ„์น˜ ๋ฐฐ์—ด(๋งˆ์ง€๋ง‰์ด ๊ฐ€์žฅ ์ตœ์‹ ) - // last๋ฅผ ๊บผ๋‚ด์„œ currentLocation์— ์ €์žฅ - currentLocation = locations.last - - // ์œ„์น˜๊ฐ€ "ํ•œ ๋ฒˆ๋งŒ" ํ•„์š”ํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ๋ฐ”๋กœ stop ํ•ด๋„ ๋จ - // ๊ณ„์† ์ถ”์ ์ด ํ•„์š”ํ•˜๋ฉด ์ด ์ค„์€ ์ง€์šฐ๊ณ  ํ•„์š” ์‹œ์ ์— stop() ํ˜ธ์ถœ - manager.stopUpdatingLocation() + guard let loc = locations.last else { return } + + currentLocation = loc + print("๐Ÿ“ ์œ„๋„:", loc.coordinate.latitude) + print("๐Ÿ“ ๊ฒฝ๋„:", loc.coordinate.longitude) + + //manager.stopUpdatingLocation() } - + + func locationManager( + _ manager: CLLocationManager, + didFailWithError error: Error + ) { + print("โŒ ์œ„์น˜ ์š”์ฒญ ์‹คํŒจ:", error.localizedDescription) + } + // locationManagerDidChangeAuthorization: ๊ถŒํ•œ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ ํ˜ธ์ถœ - // (ํ—ˆ์šฉ/๊ฑฐ๋ถ€/๋ฏธ๊ฒฐ์ • โ†’ ํ—ˆ์šฉ ๊ฐ™์€ ๋ณ€ํ™”) + // (ํ—ˆ์šฉ/๊ฑฐ๋ถ€/๋ฏธ๊ฒฐ์ • โ†’ ํ—ˆ์šฉ ๊ฐ์ง€) func locationManagerDidChangeAuthorization( _ manager: CLLocationManager ) { // ์ตœ์‹  ๊ถŒํ•œ ์ƒํƒœ๋ฅผ @Published์— ๋ฐ˜์˜ํ•ด์„œ // SwiftUI๊ฐ€ "๊ถŒํ•œ ๋ฐ”๋€œ"์„ ๊ฐ์ง€ํ•˜๋„๋ก ํ•จ authorizationStatus = manager.authorizationStatus + print("๐Ÿ”‘ ๊ถŒํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ:", authorizationStatus) + + if authorizationStatus == .authorizedWhenInUse || + authorizationStatus == .authorizedAlways { + print("๐Ÿ“ [AUTH] ๊ถŒํ•œ ํ—ˆ์šฉ โ†’ ์ž๋™ ์œ„์น˜ ์š”์ฒญ") + manager.requestLocation() + } } + + func requestOneTimeLocation() { + if authorizationStatus == .authorizedWhenInUse || + authorizationStatus == .authorizedAlways { + print("๐Ÿ“ ์œ„์น˜ ์š”์ฒญ ์‹คํ–‰") + locationManager.requestLocation() + } else if authorizationStatus == .notDetermined { + print("๐Ÿ“ ๊ถŒํ•œ ์š”์ฒญ") + locationManager.requestWhenInUseAuthorization() + } else { + print("โŒ ์œ„์น˜ ๊ถŒํ•œ ๊ฑฐ๋ถ€๋จ") + } + } +} // (์„ ํƒ) ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ์ฝœ๋ฐฑ๋„ ๊ตฌํ˜„ ๊ฐ€๋Šฅ // ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ๋ชป ์“ฐ๋Š” ์ƒํ™ฉ(๊ถŒํ•œ ๊ฑฐ๋ถ€, ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ ๋“ฑ)์—์„œ ์œ ์šฉ - func locationManager( - _ manager: CLLocationManager, - didFailWithError error: Error - ) { - // ์—๋Ÿฌ ๋กœ๊ทธ ์ถœ๋ ฅ(๋””๋ฒ„๊น…์šฉ) - print("Location error:", error.localizedDescription) - } -} +// func locationManager( +// _ manager: CLLocationManager, +// didFailWithError error: Error +// ) { +// // ์—๋Ÿฌ ๋กœ๊ทธ ์ถœ๋ ฅ(๋””๋ฒ„๊น…์šฉ) +// print("Location error:", error.localizedDescription) +// } //#Preview { // LocationService() diff --git a/Shoppingmate_Frontend/Service/ScanService.swift b/Shoppingmate_Frontend/Service/ScanService.swift new file mode 100644 index 0000000..88bfdb5 --- /dev/null +++ b/Shoppingmate_Frontend/Service/ScanService.swift @@ -0,0 +1,198 @@ +// +// ScanService.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/8/26. +// + +import Foundation + +enum APIError: Error, LocalizedError { + case invalidURL + case httpStatus(Int, String) + case transport(Error) + + var errorDescription: String? { + switch self { + case .invalidURL: + return "URL์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์•„์š”." + case .httpStatus(let code, let body): + return "์„œ๋ฒ„ ์˜ค๋ฅ˜ (HTTP \(code))\n\(body)" + case .transport(let e): + return "๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜: \(e.localizedDescription)" + } + } +} + +//scan post +final class ScanService { + static let shared = ScanService() + + func uploadScans( + userId: Int, + items: [ScanUploadItem] + ) async throws { + + let baseURL = baseURL.base.rawValue + guard let url = URL(string: "\(baseURL)/scan") else { + print("โŒ [SCAN] URL ์ƒ์„ฑ ์‹คํŒจ") + throw APIError.invalidURL +// print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") +// throw URLError(.badURL) + } + + let body = ScanUploadRequest(userId: userId,items: items) + let jsonData = try JSONEncoder().encode(body) + + // ๐Ÿ”Ž ์š”์ฒญ ๋กœ๊ทธ + print("โ—๏ธ [SCAN REQUEST]") + print("URL:", url.absoluteString) + print("Method: POST") + print("Body:", String(data: jsonData, encoding: .utf8) ?? "nil") + + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.timeoutInterval = 60 + + do { + let (data, response) = try await URLSession.shared.data(for: request) + + let bodyText = String(data: data, encoding: .utf8) ?? "" + print("๐Ÿ“ฆ Scan POST Response Body:", bodyText) + + guard let httpResponse = response as? HTTPURLResponse else { + print("โŒ HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") + throw URLError(.badServerResponse) + } + + // ๐Ÿ”Ž ์‘๋‹ต ๋กœ๊ทธ + print("๐Ÿ“ฅ [SCAN RESPONSE]") + print("StatusCode:", httpResponse.statusCode) + print("Body:", bodyText) + + guard (200...299).contains(httpResponse.statusCode) else { + throw APIError.httpStatus(httpResponse.statusCode, bodyText) + } + + print("โœ… uploadScans ์„ฑ๊ณต") + }catch{ + throw APIError.transport(error) + } + } + + + + // scan get + func fetchScans(userId: Int) async throws -> [ScanItemResponse] { + let baseURL = baseURL.base.rawValue + + guard var components = URLComponents(string: "\(baseURL)/scan") else { + print("โŒ [SCAN GET] URLComponents ์ƒ์„ฑ ์‹คํŒจ") + throw APIError.invalidURL + } + + components.queryItems = [ + URLQueryItem(name: "userId", value: String(userId)) + ] + + guard let url = components.url else { + print("โŒ [SCAN GET] URL ์ƒ์„ฑ ์‹คํŒจ") + throw APIError.invalidURL + } + + // ๐Ÿ”Ž ์š”์ฒญ ๋กœ๊ทธ + print("โ—๏ธ [SCAN GET REQUEST]") + print("URL:", url.absoluteString) + print("Method: GET") + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.timeoutInterval = 60 + + do { + let (data, response) = try await URLSession.shared.data(for: request) + + let bodyText = String(data: data, encoding: .utf8) ?? "" + guard let httpResponse = response as? HTTPURLResponse else { + print("โŒ [SCAN GET] HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") + throw URLError(.badServerResponse) + } + + // ๐Ÿ”Ž ์‘๋‹ต ๋กœ๊ทธ + print("๐Ÿ“ฅ [SCAN GET RESPONSE]") + print("StatusCode:", httpResponse.statusCode) + print("Body:", bodyText) + + guard (200...299).contains(httpResponse.statusCode) else { + throw APIError.httpStatus(httpResponse.statusCode, bodyText) + } + + let decoded = try JSONDecoder().decode([ScanItemResponse].self, from: data) + print("โœ… fetchScans ์„ฑ๊ณต: \(decoded.count)๊ฐœ") + return decoded + + } catch { + throw APIError.transport(error) + } + } + + + // PATCH /scan/hide?userId=1 + func hideScans(userId: Int) async throws { + let baseURL = baseURL.base.rawValue + + guard var components = URLComponents(string: "\(baseURL)/scan/hide") else { + print("โŒ [SCAN HIDE] URLComponents ์ƒ์„ฑ ์‹คํŒจ") + throw APIError.invalidURL + } + + components.queryItems = [ + URLQueryItem(name: "userId", value: String(userId)) + ] + + guard let url = components.url else { + print("โŒ [SCAN HIDE] URL ์ƒ์„ฑ ์‹คํŒจ") + throw APIError.invalidURL + } + + // ๐Ÿ”Ž ์š”์ฒญ ๋กœ๊ทธ + print("โ—๏ธ [SCAN HIDE REQUEST]") + print("URL:", url.absoluteString) + print("Method: PATCH") + + var request = URLRequest(url: url) + request.httpMethod = "PATCH" + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.timeoutInterval = 60 + + do { + let (data, response) = try await URLSession.shared.data(for: request) + + let bodyText = String(data: data, encoding: .utf8) ?? "" + guard let httpResponse = response as? HTTPURLResponse else { + print("โŒ [SCAN HIDE] HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") + throw URLError(.badServerResponse) + } + + // ๐Ÿ”Ž ์‘๋‹ต ๋กœ๊ทธ + print("๐Ÿ“ฅ [SCAN HIDE RESPONSE]") + print("StatusCode:", httpResponse.statusCode) + print("Body:", bodyText) + + guard (200...299).contains(httpResponse.statusCode) else { + throw APIError.httpStatus(httpResponse.statusCode, bodyText) + } + + print("โœ… hideScans ์„ฑ๊ณต (isShown=false ์ฒ˜๋ฆฌ๋จ)") + } catch let apiError as APIError { + throw apiError // โœ… statusCode ๋ณด์กด + } catch { + throw APIError.transport(error) + } + } + +} diff --git a/Shoppingmate_Frontend/Service/UploadService.swift b/Shoppingmate_Frontend/Service/UploadService.swift index 978f833..d8e4105 100644 --- a/Shoppingmate_Frontend/Service/UploadService.swift +++ b/Shoppingmate_Frontend/Service/UploadService.swift @@ -8,46 +8,245 @@ import Foundation// ๋„คํŠธ์›Œํฌ, JSON, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋“ฑ final class UploadService { + + //์‚ฌ์šฉ์ž ์ขŒํ‘œ POST +// func uploadLocation( +// location: LocationDTO? +// ) async throws { +// // URL ์ƒ์„ฑ +// let baseURL = baseURL.base.rawValue +// guard let url = URL(string: "\(baseURL)/users/location") else { +// print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") +// throw URLError(.badURL) +// } +// +// // LocationDTO โ†’ JSON +// let jsonData = try JSONEncoder().encode(location) +// +// // URLRequest ์„ค์ • +// var request = URLRequest(url: url) +// request.httpMethod = "POST" +// request.httpBody = jsonData +// request.setValue("application/json", forHTTPHeaderField: "Content-Type") +// +// logRequest(request) +// +// // ๋„คํŠธ์›Œํฌ ์š”์ฒญ +// let (data, response) = try await URLSession.shared.data(for: request) +// +// // ์‘๋‹ต ๊ฒ€์ฆ +// guard let httpResponse = response as? HTTPURLResponse else { +// print("โŒ HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") +// throw URLError(.badServerResponse) +// } +// print("๐Ÿ“ฅ StatusCode:", httpResponse.statusCode) +// +// if !(200...299).contains(httpResponse.statusCode) { +// if let errorBody = String(data: data, encoding: .utf8) { +// print("โŒ Server Error Body:", errorBody) +// } +// throw URLError(.badServerResponse) +// } +// +// print("โœ… uploadLocation ์„ฑ๊ณต") +// } + + //UUID POST + func uploadUUID( + uuid: UUIDDTO + ) async throws -> UserIdResponse { + // URL ์ƒ์„ฑ + let baseURL = baseURL.base.rawValue + guard let url = URL(string: "\(baseURL)/user/login") else { + print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") + throw URLError(.badURL) + } + + // UUIDDTO โ†’ JSONใ„ฑ + let jsonData = try JSONEncoder().encode(uuid) + + // URLRequest ์„ค์ • + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + //logRequest(request) + // ๋„คํŠธ์›Œํฌ ์š”์ฒญ + let (data, response) = try await URLSession.shared.data(for: request) + + if let body = String(data: data, encoding: .utf8) { + print("๐Ÿ“ฆ UUID POST Response Body:", body) + } + + // ์‘๋‹ต ๊ฒ€์ฆ + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + print("โŒ HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") + throw URLError(.badServerResponse) + } + print("๐Ÿ“ฅ StatusCode:", httpResponse.statusCode) - func upload( - imageData: Data, - recognizedText: String, - location: LocationDTO? - ) async throws { + let decoded = try JSONDecoder().decode(UserIdResponse.self, from: data) - // ์„œ๋ฒ„๋กœ ๋ณด๋‚ผ JSON body - var body: [String: Any] = [ - "text": recognizedText - ] + UserDefaults.standard.set(decoded.userId, forKey: "userId") + //UserDefaults.standard.set(decoded.id, forKey: "userId") + + print("โœ… uploadUUID ์„ฑ๊ณต") + return decoded + } - // ์œ„์น˜๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ JSON์— ํฌํ•จ - if let location { - body["latitude"] = location.latitude - body["longitude"] = location.longitude + //UUID GET + //์ด๊ฑฐ ์•„์ง ํ˜ธ์ถœ ์•ˆํ•จ. ์–ด๋””์„œ getํ• ๊ฑด์ง€ ์ •ํ•˜๊ธฐ +// func fetchUserInfo(uuid: String) async throws { +// // URL ์ƒ์„ฑ +// let baseURL = baseURL.base.rawValue +// guard let url = URL(string: "\(baseURL)/users/{uuid}") else { +// print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") +// throw URLError(.badURL) +// } +// +// // URLRequest ์„ค์ • +// var request = URLRequest(url: url) +// request.httpMethod = "GET" +// +// logRequest(request) +// +// // ๋„คํŠธ์›Œํฌ ์š”์ฒญ +// let (data, response) = try await URLSession.shared.data(for: request) +// +// // ์‘๋‹ต ๊ฒ€์ฆ +// guard let httpResponse = response as? HTTPURLResponse else { +// print("โŒ HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") +// throw URLError(.badServerResponse) +// } +// print("๐Ÿ“ฅ StatusCode:", httpResponse.statusCode) +// +// if !(200...299).contains(httpResponse.statusCode) { +// if let errorBody = String(data: data, encoding: .utf8) { +// print("โŒ Server Error Body:", errorBody) +// } +// throw URLError(.badServerResponse) +// } +// +// if let body = String(data: data, encoding: .utf8) { +// print("๐Ÿ“ฆ Response Body:", body) +// } +// +// print("โœ… fetchUserInfo ์„ฑ๊ณต") +// } + + //Location UPDATE + func updateLocation( + location: LocationDTO + ) async throws { + // URL ์ƒ์„ฑ + let baseURL = baseURL.base.rawValue + guard let url = URL(string: "\(baseURL)/user/update-location") else { + print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") + throw URLError(.badURL) } - // Swift Dictionary -> JSON Data - let jsonData = try JSONSerialization.data(withJSONObject: body) - - // Swagger ๋ณด๊ณ  ์—”๋“œํฌ์ธํŠธ ๋‹ค์‹œ ๋„ฃ๊ธฐ(๋„ฃ์œผ๋ฉด ! ๋นผ๊ธฐ) - var request = URLRequest(url: URL(string: "https://your.api/upload")!) - request.httpMethod = "POST" + // LocationDTO โ†’ JSON + let jsonData = try JSONEncoder().encode(location) + + // URLRequest ์„ค์ • + var request = URLRequest(url: url) + request.httpMethod = "PATCH" request.httpBody = jsonData request.setValue("application/json", forHTTPHeaderField: "Content-Type") - // ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ - // URLSession.shared: ๊ธฐ๋ณธ ์„ธ์…˜ - // .data(for:): ์š”์ฒญ ์ „์†ก - // await: ์‘๋‹ต ์˜ฌ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ - // try: ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ์‹œ throw - let (_, response) = try await URLSession.shared.data(for: request) + logRequest(request) + + // ๋„คํŠธ์›Œํฌ ์š”์ฒญ + let (data, response) = try await URLSession.shared.data(for: request) + + // ์‘๋‹ต ๊ฒ€์ฆ + guard let httpResponse = response as? HTTPURLResponse else { + print("โŒ HTTPResponse ์บ์ŠคํŒ… ์‹คํŒจ") + throw URLError(.badServerResponse) + } + print("๐Ÿ“ฅ StatusCode:", httpResponse.statusCode) - guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { + if !(200...299).contains(httpResponse.statusCode) { + if let errorBody = String(data: data, encoding: .utf8) { + print("โŒ Server Error Body:", errorBody) + } throw URLError(.badServerResponse) } + print("โœ… updatedLocation ์„ฑ๊ณต") } } +//gemini GET +func getGemini(scanId: Int) async throws -> DetailResponse { + // URL ์ƒ์„ฑ + let baseURL = baseURL.base.rawValue + + guard let url = URL(string: "\(baseURL)/gemini/\(scanId)") else { + print("โŒ URL ์ƒ์„ฑ ์‹คํŒจ") + throw URLError(.badURL) + } + + // URLRequest ์„ค์ • + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Accept") + + logRequest(request) + + // ๋„คํŠธ์›Œํฌ ์š”์ฒญ + let (data, response) = try await URLSession.shared.data(for: request) + + // ์‘๋‹ต ๊ฒ€์ฆ + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + print("โŒ Server Error:", String(data: data, encoding: .utf8) ?? "") + throw URLError(.badServerResponse) + } + + if let body = String(data: data, encoding: .utf8) { + print("๐Ÿ“ฆ Raw JSON:", body) + } + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + do { + let decoded = try decoder.decode(DetailResponse.self, from: data) + print("โœ… get Gemini info ์„ฑ๊ณต") + return decoded + } catch { + print("โŒ Decoding Error:", error) + throw error + } +} +// if let body = String(data: data, encoding: .utf8) { +// print("๐Ÿ“ฆ Response Body:", body) +// } +// +// let decoded = try JSONDecoder().decode(DetailResponse.self, from: data) +// print("โœ… get Gemini info ์„ฑ๊ณต") +// return decoded + + +//๋””๋ฒ„๊น…์šฉ ๋กœ๊ทธํ•จ์ˆ˜ +private func logRequest(_ request: URLRequest) { + print("โ—๏ธ [REQUEST]")//์ด ์•„๋ž˜๋ถ€ํ„ฐ ์š”์ฒญ ๋กœ๊ทธ๋ผ๋Š” ๊ฒƒ ๊ตฌ๋ณ„ + print("URL:", request.url?.absoluteString ?? "nil")//request.url: ์ด ์š”์ฒญ์ด ๊ฐ€๋Š” URL ๊ฐ์ฒด(์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜) + print("Method:", request.httpMethod ?? "nil") + + //body๊ฐ€ ์žˆ์œผ๋ฉด ์ถœ๋ ฅํ•˜๊ณ , ์—†์œผ๋ฉด ์—†์Œ ์ถœ๋ ฅ + if let body = request.httpBody, + let bodyString = String(data: body, encoding: .utf8) { + print("Body:", bodyString) + } else { + print("Body: ์—†์Œ") + } +} + + + //#Preview { // UploadService() //} diff --git a/Shoppingmate_Frontend/Shoppingmate_FrontendApp.swift b/Shoppingmate_Frontend/Shoppingmate_FrontendApp.swift index 43eb390..66e1d21 100644 --- a/Shoppingmate_Frontend/Shoppingmate_FrontendApp.swift +++ b/Shoppingmate_Frontend/Shoppingmate_FrontendApp.swift @@ -9,9 +9,30 @@ import SwiftUI @main struct Shoppingmate_FrontendApp: App { + @StateObject private var loginViewModel = LoginViewModel() + @StateObject private var serverViewModel: ServerViewModel +// @StateObject private var serverViewModel = ServerViewModel() + + init() { + let loginVM = LoginViewModel() + _loginViewModel = StateObject(wrappedValue: loginVM) + _serverViewModel = StateObject(wrappedValue: ServerViewModel(loginViewModel: loginVM)) + } +// init() { +// let loginViewModel = LoginViewModel() +// _serverViewModel = StateObject( +// wrappedValue: ServerViewModel(loginViewModel: loginViewModel) +// ) +// _loginViewModel = StateObject(wrappedValue: loginViewModel) +// } + var body: some Scene { WindowGroup { - CameraOCRView() + NavigationStack { + OnboardingView() + .environmentObject(loginViewModel) + .environmentObject(serverViewModel) + } } } } diff --git a/Shoppingmate_Frontend/String+Extension.swift b/Shoppingmate_Frontend/String+Extension.swift new file mode 100644 index 0000000..43e8ae5 --- /dev/null +++ b/Shoppingmate_Frontend/String+Extension.swift @@ -0,0 +1,14 @@ +// +// String+Extension.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/9/26. +// + +import Foundation + +extension String { + var removingDoubleAsterisks: String { + self.replacingOccurrences(of: "**", with: "") + } +} diff --git a/Shoppingmate_Frontend/View/CameraOCRView.swift b/Shoppingmate_Frontend/View/CameraOCRView.swift deleted file mode 100644 index ac94f24..0000000 --- a/Shoppingmate_Frontend/View/CameraOCRView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// CameraOCRView.swift -// Shoppingmate_Frontend -// -// Created by Jinsoo Park on 12/26/25. -// - -import SwiftUI - -struct CameraOCRView: View { - - @StateObject private var camera = CameraManager() - - var body: some View { - ZStack { - - // UIKit PreviewLayer ์ƒ์„ฑ - // CameraManager๋กœ ์ „๋‹ฌ(์ขŒํ‘œ ๋ณ€ํ™˜์šฉ) - CameraPreview(session: camera.session) { layer in - camera.previewLayer = layer - } - .ignoresSafeArea() - - // ROI ๊ณ„์‚ฐ -> CameraManager์— ์ „๋‹ฌ - ROIOverlay { rect in - camera.updateROIRect(rect) - } - - // ํ•˜๋‹จ ์ปจํŠธ๋กค - VStack { - Spacer() - - if camera.isProcessing { - ProgressView("OCR ์ค‘...") - .padding() - } - - Button { - camera.capturePhoto() - } label: { - Circle() - .fill(.white) - .frame(width: 72, height: 72) - .overlay( - Circle().stroke(.black, lineWidth: 2) - ) - } - .padding(.bottom, 30) - } - - // ๊ฒฐ๊ณผ ํ‘œ์‹œ - if !camera.recognizedText.isEmpty { - VStack { - Spacer() - Text(camera.recognizedText) - .padding() - .background(.ultraThinMaterial) - .cornerRadius(12) - .padding() - } - } - } - .onAppear { camera.startSession() } - .onDisappear { camera.stopSession() } - } -} diff --git a/Shoppingmate_Frontend/View/CameraPreview.swift b/Shoppingmate_Frontend/View/CameraPreview.swift deleted file mode 100644 index b63c930..0000000 --- a/Shoppingmate_Frontend/View/CameraPreview.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CameraPreview.swift -// Shoppingmate_Frontend -// -// Created by Jinsoo Park on 12/26/25. -// - -import SwiftUI -import AVFoundation - -// UIView ์ž์ฒด์˜ layer๋ฅผ AVCaptureVideoPreviewLayer๋กœ ์‚ฌ์šฉํ•˜๋Š” ์ •์„ ๊ตฌํ˜„ -final class PreviewView: UIView { - override class var layerClass: AnyClass { - AVCaptureVideoPreviewLayer.self - } - - var previewLayer: AVCaptureVideoPreviewLayer { - return layer as! AVCaptureVideoPreviewLayer - } -} - -struct CameraPreview: UIViewRepresentable { - - let session: AVCaptureSession - let onLayerReady: (AVCaptureVideoPreviewLayer) -> Void - - func makeUIView(context: Context) -> PreviewView { - let view = PreviewView() - view.backgroundColor = .black - - // ๐Ÿ”‘ previewLayer์— ์„ธ์…˜์„ ์ง์ ‘ ์—ฐ๊ฒฐ (๊ฐ€์žฅ ์ค‘์š”) - view.previewLayer.session = session - view.previewLayer.videoGravity = .resizeAspectFill - - // CameraManager์— previewLayer ์ „๋‹ฌ - DispatchQueue.main.async { - onLayerReady(view.previewLayer) - } - - return view - } - - func updateUIView(_ uiView: PreviewView, context: Context) { - // session์ด ๋ฐ”๋€Œ๋Š” ๊ฒฝ์šฐ ๋Œ€๋น„ (๊ฑฐ์˜ ์•ˆ ๋ฐ”๋€œ) - if uiView.previewLayer.session !== session { - uiView.previewLayer.session = session - } - } -} diff --git a/Shoppingmate_Frontend/View/CameraView/CameraOCRView.swift b/Shoppingmate_Frontend/View/CameraView/CameraOCRView.swift new file mode 100644 index 0000000..c02013a --- /dev/null +++ b/Shoppingmate_Frontend/View/CameraView/CameraOCRView.swift @@ -0,0 +1,500 @@ +//// +//// CameraOCRView.swift +//// Shoppingmate_Frontend +//// +//// Created by Jinsoo Park on 12/26/25. +//// +// +import SwiftUI + +struct CameraOCRView: View { + @StateObject private var camera = CameraManager() + + let cameFromMap: Bool + let userIdResponse: UserIdResponse? // userID ์—…๋กœ๋“œ + + init( + cameFromMap: Bool, + userIdResponse: UserIdResponse? = nil + ) { + self.cameFromMap = cameFromMap + self.userIdResponse = userIdResponse + } + + @State private var ParseFail = false // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์ถœ๋ ฅ ๋ฌธ๊ตฌ + @State private var ocrBeforeCount: Int = 0 // OCR ์ดฌ์˜ ์ €์žฅ ํ™•์ธ์šฉ (๋ฌธ๊ตฌ) + @State private var didTapCapture = false // OCR ์ดฌ์˜ ์ €์žฅ ํ™•์ธ์šฉ + + + @State private var goResult = false //๊ฒฐ๊ณผ ํ™”๋ฉด ์ด๋™ ์—ฌ๋ถ€ + @State private var products: [RecognizedProduct] = [] // ๊ฒฐ๊ณผํ™”๋ฉด์— ๋„˜๊ธธ ์‹ค์ œ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ + @State private var goToMap = false + + @State private var roiOverlayID = UUID() // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์šฉ UUID ๊ด€์ฐฐ + + //์—…๋กœ๋“œ UI ์ƒํƒœ + @State private var isUploading = false + @State private var showUploadError = false + @State private var uploadErrorMessage = "" + + //์ดฌ์˜ ๊ฒฐ๊ณผ ๋ณด์—ฌ์ฃผ๊ธฐ + @State private var showToast = false + @State private var toastText = "" + @State private var toastWorkItem: DispatchWorkItem? + @State private var lastFilterCount = 0 // append์ผ ๋•Œ๋งŒ ํ† ์ŠคํŠธ ๋„์šฐ๊ธฐ ์šฉ + + @State private var didSendHide = false // hide์ „์šฉ + + @Environment(\.dismiss) private var dismiss + @Environment(\.scenePhase) private var scenePhase + + var body: some View { + ZStack { + // ์นด๋ฉ”๋ผ ํ”„๋ฆฌ๋ทฐ + CameraPreview(session: camera.session) { layer in + camera.previewLayer = layer + } + .ignoresSafeArea() + //์ง€๋„๋กœ ๊ฐ€๋Š” ๋ฒ„ํŠผ + VStack { + HStack { + Button { + if cameFromMap { + dismiss() // ๋กœ๊ทธ์ธ โ†’ ์ง€๋„ โ†’ ์นด๋ฉ”๋ผ + } else { + goToMap = true // ๋กœ๊ทธ์ธ ์•ˆ ๊ฑฐ์นœ ๊ฒฝ์šฐ + } + } label: { + Image("goMap") + .resizable() + .frame(width: 130, height: 40) + .padding(5) + } + .padding(.leading, 5) + .padding(.top, 5) + + Spacer() + } + Spacer() + } + .overlay(alignment: .center) { + ROIOverlay(ParseFail: $ParseFail) + .id(roiOverlayID) // id ๋ฐ”๋€Œ๋ฉด ROI ์žฌ์ƒ์„ฑ + .frame(maxWidth: .infinity, maxHeight: .infinity) // ํ”„๋ฆฌ๋ทฐ ์ „์ฒด ํฌ๊ธฐ ๋ฐ›๊ธฐ + .ignoresSafeArea() // ์นด๋ฉ”๋ผ ํ”„๋ฆฌ๋ทฐ๋ž‘ ์ขŒํ‘œ ๋งž์ถ”๊ธฐ + .allowsHitTesting(false) + } + + // ํ•˜๋‹จ ๋ฒ„ํŠผ ๊ตฌ์—ญ + VStack { + Spacer() + + if camera.isProcessing { //๋กœ๋”ฉ ํ‘œ์‹œ + ProgressView("OCR ์ค‘...") + .padding() + } + if isUploading { + ProgressView("์„œ๋ฒ„ ์ „์†ก ์ค‘...") + .padding() + } + + //์ฐ์€ ์‚ฌ์ง„ ์ธ๋„ค์ผ ํ‘œ์‹œ + if !camera.capturedROIImages.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(camera.capturedROIImages.indices, id: \.self) { i in + ZStack{ + // ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ + Image(uiImage: camera.capturedROIImages[i]) + .resizable() + .scaledToFill() + .frame(width: 48, height: 48) + .clipped() + .contentShape(Rectangle()) + .onTapGesture { + camera.deleteCaptured(at: i) + } + } + .overlay(alignment: .topTrailing) { + // ์šฐ์ธก ์ƒ๋‹จ X ๋ฒ„ํŠผ + Button { + camera.deleteCaptured(at: i) + } label: { + Image("LegendDelete") + .resizable() + .scaledToFit() + .frame(width:21,height:21) + } + .offset(x: 9, y: -9) + } + } + } + .padding(.leading, 30) + .padding(.trailing, 16) + .padding(.top,10) + } + .frame(height: 70) + .offset(y: -30) + } + + ZStack{ + Button { //์นด๋ฉ”๋ผ ๋ฒ„ํŠผ + ocrBeforeCount = camera.OCRFilters.count + didTapCapture = true + camera.capturePhoto() + + // guard !camera.isProcessing else { return } // ์—ฐํƒ€ ์‹œ ๊ผฌ์ž„ ๋ฐฉ์ง€ + +// camera.capturePhoto() // ParseFail ์•ˆํ•˜๋ฉด ์ด๊ฑฐ๋งŒใ„ฑ + } label: { + ZStack{ + Circle() + .fill( + LinearGradient( + stops: [ + .init(color: Color(red: 0.25, green: 0.28, blue: 0.61), location: 0.0), + .init(color: Color(red: 0.66, green: 0.68, blue: 1.0), location: 1.0) + ], + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(width: 80, height: 80) + .shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 4) + .shadow(color: .black.opacity(0.1), radius: 7.5, x: 0, y: 10) + .overlay( + Circle() + .inset(by: 2) + .stroke(.white, lineWidth: 4) + ) + Circle() + .fill(Color.white) + .frame(width: 64, height: 64) + } + } + // .disabled(camera.isProcessing) //์—ฐํƒ€ ์‹œ ๊ผฌ์ž„ ๋ฐฉ์ง€ + // .opacity(camera.isProcessing ? 0.6 : 1.0) //(์„ ํƒ) ๋น„ํ™œ์„ฑ ์‹œ ์‹œ๊ฐ ํ”ผ๋“œ๋ฐฑ + + HStack(alignment: .center) { //check button + Spacer() + Button { //์‚ฌ์ง„ ์ด๋™ ์ฒดํฌ ๋ฒ„ํŠผ + // guard !camera.isProcessing else { return } //์—ฐํƒ€ ์‹œ ๊ผฌ์ž„ ๋ฐฉ์ง€ + // guard !camera.capturedROIImages.isEmpty else { return } + // goResult = true + +// if !camera.capturedROIImages.isEmpty { +// goResult = true +// } + + print("โœ… ์ฒดํฌ ๋ฒ„ํŠผ ๋ˆŒ๋ฆผ") + Text("filters:\(camera.OCRFilters.count) proc:\(camera.isProcessing ? "T":"F") up:\(isUploading ? "T":"F")") + .font(.caption2) + .foregroundStyle(.white) + + + + Task { await uploadAndGoResult() } + + } label: { + Image("Check") + .resizable() + .scaledToFit() + .frame(width: 26, height: 26) + .padding(11) + .background( + camera.capturedROIImages.isEmpty + ? Color(red: 0.4, green: 0.4, blue: 0.4) + : Color(red: 0.25, green: 0.28, blue: 0.61) + ) + .clipShape(Circle()) + } + // .disabled(camera.capturedROIImages.isEmpty || camera.isProcessing) // ์—ฐํƒ€ ์‹œ ๊ผฌ์ž„ ๋ฐฉ์ง€ + // .opacity((camera.capturedROIImages.isEmpty || camera.isProcessing) ? 0.6 : 1.0) + +// .disabled(camera.OCRFilters.isEmpty) // OCRFilter ๊ฐ’ ์—†์œผ๋ฉด ๋น„ํ™œ์„ฑ + +// .disabled(camera.capturedROIImages.isEmpty) // ROI ์ด๋ฏธ์ง€ ์—†์œผ๋ฉด ๋น„ํ™œ์„ฑ + .disabled(camera.OCRFilters.isEmpty || camera.isProcessing || isUploading) + + .padding(.trailing, 20) // ์šฐ์ธก ์—ฌ๋ฐฑ + + } //HStack ์ฒดํฌ ๋ฒ„ํŠผ + } // ZStack buttons + .padding(.bottom, 33) // bottom safearea 34pt + + } // VStack ํ•˜๋‹จ ๋ฒ„ํŠผ ๊ตฌ์—ญ + + +// // ๊ฒฐ๊ณผ ํ‘œ์‹œ (OCR์ธ์‹ ํ™•์ธ์šฉ) +// if !camera.recognizedText.isEmpty { +// VStack { +// Spacer() +// Text(camera.recognizedText) +// .padding() +// .background(.ultraThinMaterial) +// .cornerRadius(12) +// .padding() +// } +// } + +// // ๊ฒฐ๊ณผ ํ‘œ์‹œ (OCR Filter ์ ์šฉ) +// if !camera.OCRFilters.isEmpty { +// VStack(alignment: .leading, spacing: 8) { +// Text("๐Ÿ“ฆ Captured Items") +// .font(.headline) +// +// ForEach(camera.OCRFilters) { item in +// VStack(alignment: .leading, spacing: 4) { +// Text("์ƒํ’ˆ๋ช…: \(item.name)") +// .font(.subheadline) +// +// Text("๊ฐ€๊ฒฉ: \(String(item.price))์›") +// .font(.caption) +// .foregroundColor(.secondary) +// } +// .padding(8) +// .background(.ultraThinMaterial) +// .cornerRadius(8) +// } +// } +// .padding() +// } + + // ์ดฌ์˜ ๊ฒฐ๊ณผ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ + if showToast { + VStack { + Text(toastText) + .font(.subheadline) + .multilineTextAlignment(.leading) + .padding(.horizontal, 14) + .padding(.vertical, 12) + .background(.ultraThinMaterial) + .cornerRadius(12) + .padding(.top,80) + } + .transition(.opacity) + } + + + + } //ZStack all + .onChange(of: camera.isProcessing) { isProcessing in //ํŒŒ์‹ฑ ์‹œ ์•ˆ๋‚ด ๋ฌธ๊ตฌ ์ถœ๋ ฅ + // processing์ด ๋๋‚˜๋Š” ์ˆœ๊ฐ„๋งŒ ์ฒดํฌ + guard didTapCapture, isProcessing == false else { return } + didTapCapture = false + // ์ดฌ์˜ ์ „ํ›„ count๊ฐ€ ๊ฐ™์œผ๋ฉด "์ถ”๊ฐ€๊ฐ€ ์•ˆ ๋œ ๊ฒƒ" โ†’ ์‹คํŒจ ๋ฌธ๊ตฌ + if camera.OCRFilters.count == ocrBeforeCount { + handleParseFail() + } + } + .onChange(of: camera.OCRFilters.count) { _, newCount in + // ์ถ”๊ฐ€(append)์ผ ๋•Œ๋งŒ ํ† ์ŠคํŠธ + guard newCount > lastFilterCount else { + lastFilterCount = newCount + return + } + lastFilterCount = newCount + + guard let last = camera.OCRFilters.last else { return } + + toastText = "์ƒํ’ˆ๋ช…: \(last.name)\n๊ฐ€๊ฒฉ: \(last.price)์›" + + toastWorkItem?.cancel() + withAnimation(.easeOut(duration: 0.2)) { showToast = true } + + let work = DispatchWorkItem { + withAnimation(.easeOut(duration: 0.5)) { showToast = false } + toastText = "" + } + toastWorkItem = work + DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: work) + } + + + .navigationDestination(isPresented: $goResult) { + RecognitionResultView( + products: products, + userId: userIdResponse?.userId + ) + } + .navigationDestination(isPresented: $goToMap) { + if let userIdResponse { + LocationSelectView(userIdResponse: userIdResponse) + } + } + .navigationBarBackButtonHidden(true) + .onAppear { + camera.startSession() + roiOverlayID = UUID() // ์นด๋ฉ”๋ผ ํŽ˜์ด์ง€ ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋‹ค์‹œ + } + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + didSendHide = false // ๋‹ค์Œ๋ฒˆ ํ…Œ์ŠคํŠธ/์žฌ์ง„์ž… ๋•Œ ๋‹ค์‹œ ๋ณด๋‚ด๊ธฐ + } + + // โœ… background๋Š” ๋„ˆ๋ฌด ๋Šฆ์–ด์„œ ์‘๋‹ต ๋กœ๊ทธ๊ฐ€ ์•ˆ ์ฐํž ์ˆ˜ ์žˆ์Œ โ†’ inactive์—์„œ ๋จผ์ € ๋ณด๋ƒ„ + guard newPhase == .inactive else { return } + triggerHideIfNeeded(source: "scenePhase.inactive", verify: true) + + } + + + + // .onDisappear { camera.stopSession() } //๋’ค๋กœ ๊ฐˆ ๋•Œ ์นด๋ฉ”๋ผ ๊นœ๋นก์ž„ ์žˆ์–ด์„œ ์ผ๋‹จ ๊บผ๋‘  + } // var body + +// private func makeProducts(from images: [UIImage]) -> [RecognizedProduct] { +// images.map { image in +// RecognizedProduct( +// image: image, +// brand: "ํ”ผ์ฃค", +// name: "ํ”ผ์ฃค ์‹ค๋‚ด๊ฑด์กฐ ์„ฌ์œ ์œ ์—ฐ์ œ ๋ผ๋ฒค๋”ํ–ฅ", +// amount: "2.5L", +// price: "12,800์›", +// onlinePrice: "15,000์›", +// perUse: "ํ•œ๋ฒˆ ์‚ฌ์šฉ 283์›๊ผด" +// ) +// } +// } + + private func handleParseFail() { // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋ฌธ๊ตฌ + guard ParseFail == false else { return } + ParseFail = true + + // 1์ดˆ ํ›„ fade out + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + withAnimation(.easeOut(duration: 0.3)) { + ParseFail = false + } + } + } + + @MainActor + private func uploadAndGoResult() async { // ์„œ๋ฒ„ ์—…๋กœ๋“œ ํ•จ์ˆ˜ + print("๐Ÿš€ uploadAndGoResult ์ง„์ž…, OCRFilters:", camera.OCRFilters.count) + guard !camera.OCRFilters.isEmpty else { return } + + isUploading = true + defer { isUploading = false } + + let items: [ScanUploadItem] = camera.OCRFilters.map { + ScanUploadItem(scanName: $0.name, scanPrice: $0.price) + } + + do { + guard let userId = userIdResponse?.userId else { + print("โŒ userIdResponse ์—†์Œ") + return + } + print("๐Ÿ“ค [SCAN] ์„œ๋ฒ„ ์—…๋กœ๋“œ ์‹œ์ž‘") + + // 1) POST /scan + print("๐Ÿ“ค [SCAN] POST ์‹œ์ž‘") + try await ScanService.shared.uploadScans( + userId: userId, + items: items + ) + + // 2๏ธโƒฃ GET /scan + print("๐Ÿ“ฅ [SCAN] GET ์‹œ์ž‘") + let scanList = try await ScanService.shared.fetchScans( + userId: userId + ) + + let visible = scanList.filter { $0.isShown } + + // 3) ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฅผ RecognizedProduct๋กœ ๋ณ€ํ™˜ + self.products = visible.map { scan in + RecognizedProduct( + image: nil, // ์„œ๋ฒ„ URL๋กœ ๊ทธ๋ฆด ๊ฑฐ๋ผ nil + badge: "", // ํ•„์š”ํ•˜๋ฉด Best ๊ฐ€์„ฑ๋น„ ๊ฐ™์€๊ฑฐ ์„œ๋ฒ„๊ฐ€ ์ฃผ๋Š” ๋‚  ๋„ฃ์ž + brand: scan.naverBrand ?? "", + name: scan.scanName, + amount: "", // ์ง€๊ธˆ 6๊ฐœ ํ•„๋“œ์— ์—†์Œ + price: "\(scan.scanPrice)์›", + onlinePrice: scan.naverPrice.map { "\($0)์›" } ?? "-", + perUse: scan.aiUnitPrice ?? "๋ถ„์„ ์ค‘...", + imageURL: scan.naverImage, + scanId: scan.scanId + ) + } + + print("โœ… products ์„ธํŒ… ์™„๋ฃŒ: \(self.products.count)๊ฐœ โ†’ goResult ์ด๋™") + + camera.resetBatch() // ์ด๋™ ํ™•์ •๋œ ์‹œ์ ์—๋งŒ OCRView ์ƒํƒœ ๋น„์šฐ๊ธฐ + goResult = true + print("โžก๏ธ goResult ํ˜„์žฌ๊ฐ’:", goResult) + + } catch { + print("โŒ uploadAndGoResult catch:", error.localizedDescription) + uploadErrorMessage = error.localizedDescription + showUploadError = true + } + } + +// @MainActor +// private func hideAllScansWhenAppBackground() async { +// guard let userId = userIdResponse?.userId else { +// print("โŒ [SCAN HIDE] userIdResponse ์—†์Œ") +// return +// } +// +// do { +// print("๐Ÿ“ค [SCAN HIDE] ์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ โ†’ PATCH ์‹œ์ž‘") +// try await ScanService.shared.hideScans(userId: userId) +// print("โœ… [SCAN HIDE] PATCH ์™„๋ฃŒ") +// +// // โœ… ์—ฌ๊ธฐ! PATCH๊ฐ€ ์‹ค์ œ๋กœ ์ ์šฉ๋๋Š”์ง€ GET์œผ๋กœ ํ™•์ธ +// do { +// let scanList = try await ScanService.shared.fetchScans(userId: userId) +// let shownCount = scanList.filter { $0.isShown }.count +// let totalCount = scanList.count +// print("๐Ÿ”Ž [SCAN HIDE VERIFY] total:", totalCount, "shown:", shownCount) +// } catch { +// print("โš ๏ธ [SCAN HIDE VERIFY] GET ์‹คํŒจ:", error.localizedDescription) +// } +// } catch { +// print("โŒ [SCAN HIDE] PATCH ์‹คํŒจ:", error.localizedDescription) +// } +// } + + @MainActor + private func triggerHideIfNeeded(source: String, verify: Bool = true) { + guard !didSendHide else { return } + didSendHide = true + + guard let userId = userIdResponse?.userId else { + print("โŒ [SCAN HIDE] userIdResponse ์—†์Œ (\(source))") + return + } + + Task { + // โœ… ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋„คํŠธ์›Œํฌ ๋งˆ๋ฌด๋ฆฌ ์‹œ๊ฐ„ ํ™•๋ณด + let bgID = UIApplication.shared.beginBackgroundTask(withName: "scanHide") { + print("โฐ [SCAN HIDE] background time expired") + } + defer { UIApplication.shared.endBackgroundTask(bgID) } + + do { + print("๐Ÿ“ค [SCAN HIDE] \(source) โ†’ PATCH ์‹œ์ž‘ (userId=\(userId))") + try await ScanService.shared.hideScans(userId: userId) + print("โœ… [SCAN HIDE] PATCH ์™„๋ฃŒ") + + // โœ… PATCH ์ ์šฉ ์—ฌ๋ถ€ ํ™•์ธ (์›ํ•  ๋•Œ๋งŒ) + if verify { + do { + let scanList = try await ScanService.shared.fetchScans(userId: userId) + let shownCount = scanList.filter { $0.isShown }.count + print("๐Ÿ”Ž [SCAN HIDE VERIFY] total:", scanList.count, "shown:", shownCount) + } catch { + print("โš ๏ธ [SCAN HIDE VERIFY] GET ์‹คํŒจ:", error.localizedDescription) + } + } + } catch { + print("โŒ [SCAN HIDE] PATCH ์‹คํŒจ:", error.localizedDescription) + } + } + } + + +} // struct View diff --git a/Shoppingmate_Frontend/View/CameraView/CameraPreview.swift b/Shoppingmate_Frontend/View/CameraView/CameraPreview.swift new file mode 100644 index 0000000..c3a9584 --- /dev/null +++ b/Shoppingmate_Frontend/View/CameraView/CameraPreview.swift @@ -0,0 +1,60 @@ +// +// CameraPreview.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 12/26/25. +// + +import SwiftUI +import AVFoundation + +// UIKit์˜ AVCaptureVideoPreviewLayer๋ฅผ SwiftUI์—์„œ ๊ทธ๋Œ€๋กœ ์“ฐ๊ธฐ ์œ„ํ•œ ์–ด๋Œ‘ํ„ฐ (์นด๋ฉ”๋ผ๋ž‘ ํ™”๋ฉด ๊ตฌํ˜„ ์—ฐ๊ฒฐ) +///๋ ˆ์ด์–ด๋Š” ๊ทธ๋ฆฌ๊ธฐ ๋‹ด๋‹น, UIView๋Š” ์ด๋ฒคํŠธ, ๋ ˆ์ด์•„์›ƒ, ์ƒ๋ช…์ฃผ๊ธฐ ๋‹ด๋‹น(์ƒ์œ„) +final class PreviewView: UIView { //์ด UIView๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ๋Œ€์‹  AVCapture...๋ฅผ ์จ๋ผ) + override class var layerClass: AnyClass { + AVCaptureVideoPreviewLayer.self //์นด๋ฉ”๋ผ ํ”„๋ ˆ์ž„์„ ๋ฐ”๋กœ ๊ทธ๋ ค์ฃผ๋Š” ๋ ˆ์ด์–ด + } + + var previewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer //๋ฌด์กฐ๊ฑด AVCapture๋กœ ๋ ˆ์ด์–ด ๋ฐ˜ํ™˜ + } +} + +struct CameraPreview: UIViewRepresentable { //UIKit ์‚ฌ์šฉ ํ”„๋กœํ† ์ฝœ + + let session: AVCaptureSession //์นด๋ฉ”๋ผ ์„ธ์…˜ ์ฃผ์ž…์šฉ (๊ด€๋ฆฌ๋Š” CameraManager) + let onLayerReady: (AVCaptureVideoPreviewLayer) -> Void //preview ์™ธ๋ถ€๋กœ ์ „๋‹ฌ (OCR/ํฌ๋กญ) + + func makeUIView(context: Context) -> PreviewView { //๋ทฐ 1ํšŒ ํ˜ธ์ถœ + let view = PreviewView() + view.backgroundColor = .black //์—ฐ๊ฒฐ ์ „ ๊ฒ€์€ ๋ฐฐ๊ฒฝ + + // previewLayer์— ์„ธ์…˜์„ ์ง์ ‘ ์—ฐ๊ฒฐ + view.previewLayer.session = session + view.previewLayer.videoGravity = .resizeAspectFill //ํ™”๋ฉด ๊ฝ‰ ์ฑ„์›€, ์ž˜๋ฆผ ์žˆ์Œ (resizeAspect = ์‚ฌ์ง„ ์•ˆ์ž˜๋ฆฌ๋Š” ๋Œ€์‹  ์—ฌ๋ฐฑ ์žˆ์Œ) + +// //ํ”„๋ฆฌ๋ทฐ ์„ธ๋กœ๋กœ ํšŒ์ „ ๊ณ ์ • + if let conn = view.previewLayer.connection { + if conn.isVideoOrientationSupported { + conn.videoOrientation = .portrait + } else if conn.isVideoRotationAngleSupported(90) { + conn.videoRotationAngle = 90 + } + } + + + // CameraManager์— previewLayer ์ „๋‹ฌ + DispatchQueue.main.async { + onLayerReady(view.previewLayer) + } + + return view + } + + func updateUIView(_ uiView: PreviewView, context: Context) { //๋ทฐ ์žฌ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ƒํƒœ ๋™๊ธฐํ™” + if uiView.previewLayer.session !== session { // session์ด ๋ฐ”๋€Œ๋Š” ๊ฒฝ์šฐ ๋Œ€๋น„ + uiView.previewLayer.session = session + } + uiView.previewLayer.frame = uiView.bounds + } +} diff --git a/Shoppingmate_Frontend/View/CameraView/ROIOverlay.swift b/Shoppingmate_Frontend/View/CameraView/ROIOverlay.swift new file mode 100644 index 0000000..3185864 --- /dev/null +++ b/Shoppingmate_Frontend/View/CameraView/ROIOverlay.swift @@ -0,0 +1,164 @@ +// +// ROIOverlay.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 12/26/25. +// + +import SwiftUI + +// OCR ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ +struct ROIOverlay: View { + + @Binding var ParseFail: Bool // ํŒŒ์‹ฑ ์‹คํŒจ ๋ฌธ๊ตฌ + @State private var t: CGFloat = 0 // ์ž‘์€ ๋ฐ•์Šค ์• ๋‹ˆ๋ฉ”์ด์…˜ ์šฉ (1 = ์›๋ž˜ ROI) + @State private var hideImage = false // ์ด๋ฏธ์ง€ ํŽ˜์ด๋“œ์•„์›ƒ ์šฉ + @State private var dimOpacity: CGFloat = 0 // ํ™”๋ฉด ์–ด๋‘ก๊ฒŒ ํ•˜๋Š” ์˜ค๋ฒ„๋ ˆ์ด + + var body: some View { + GeometryReader { geo in // ๋ ˆ์ด์•„์›ƒ ํ™”๋ฉด ํฌ๊ธฐ ์ธ์‹ + + let endRect = roiRect(in: geo.size) // 'Roi' ๋ถˆ๋Ÿฌ์˜ค๊ธฐ (์ตœ์ข… ROI) + let startRect = makeStartRect(from: endRect) // UI ์• ๋‹ˆ๋ฉ”์ด์…˜์šฉ ์‹œ์ž‘ rect + let rect = lerpRect(from: startRect, to: endRect, t: t) // ์• ๋‹ˆ๋ฉ”์ด์…˜ rect + let imageScale: CGFloat = 0.73 + + ZStack { // ์Šค์บ” ์ง€์—ญ ์ปค์Šคํ…€ + + Color.black // ํ™”๋ฉด ์–ด๋‘ก๊ฒŒ ํ•˜๋Š” ํšจ๊ณผ + .opacity(dimOpacity) + .ignoresSafeArea() + .animation(.easeOut(duration: 0.3), value: dimOpacity) + + + Image("pricetag") + .resizable() + .scaledToFill() + .frame(width: startRect.width * imageScale, height: startRect.height * imageScale) + .clipped() + .position(x: startRect.midX, y: startRect.midY) + .opacity(hideImage ? 0 : 1) + .offset( + x: hideImage ? 0 : 0, + y: hideImage ? 0 : 0 + ) + .animation(.easeOut(duration: 0.2), value: hideImage) + + Text("๊ฐ€๊ฒฉํ‘œ๋ฅผ ์ฐ์–ด์ฃผ์„ธ์š”") + .font( + Font.custom("Pretendard-Bold", size: 22) + ) + .foregroundColor(.white) + .position( + x: startRect.midX, + y: startRect.maxY + 35 + ) + .opacity(hideImage ? 0 : 1) + .offset( + x: hideImage ? 0 : 0, + y: hideImage ? 0 : 0 + ) + .animation(.easeOut(duration: 0.2), value: hideImage) + + + // ์™ผ์ชฝ ์œ„ + CornerL() + .position(x: rect.minX, y: rect.minY) + .offset(x: 12, y: 12) + + // ์˜ค๋ฅธ์ชฝ ์œ„ + CornerL() + .rotationEffect(.degrees(90)) + .position(x: rect.maxX, y: rect.minY) + .offset(x: -12, y: 12) + + // ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ + CornerL() + .rotationEffect(.degrees(180)) + .position(x: rect.maxX, y: rect.maxY) + .offset(x: -12, y: -12) + + // ์™ผ์ชฝ ์•„๋ž˜ + CornerL() + .rotationEffect(.degrees(270)) + .position(x: rect.minX, y: rect.maxY) + .offset(x: 12, y: -12) + + if ParseFail { + Text("๊ฐ€๊ฒฉํ‘œ๋ฅผ ์ฐ์–ด์ฃผ์„ธ์š”") + .font( + Font.custom("Pretendard-Bold", size: 22) + ) + .foregroundColor(.white) + .position( + x: endRect.midX, + y: endRect.midY + ) + .transition(.opacity) + } + + } //ZStack + .onAppear { + t = 0 //๋ทฐ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ ์ž‘์€ ๋ฐ•์Šค + hideImage = false + dimOpacity = 0.6 // ์ตœ์ดˆ ๋ฐ๊ธฐ + DispatchQueue.main.asyncAfter(deadline: .now() + 1.1) { + withAnimation(.easeOut(duration: 0.3)) { + t = 1 + hideImage = true + dimOpacity = 0 // ๋ฐ๊ธฐ ์ •์ƒ + } + } + } + } //GeometryReader + .ignoresSafeArea() //ํ”„๋ฆฌ๋ทฐ๋ž‘ ํ™”๋ฉด ์ขŒํ‘œ๊ณ„ ๋งž์ถ”๊ธฐ + .allowsHitTesting(false) + } +} + +struct CornerL: View { + let length: CGFloat = 21 + let thickness: CGFloat = 10 + + var body: some View { + Path { path in + path.move(to: CGPoint(x: 0, y: length)) + path.addLine(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: length, y: 0)) + } + .stroke( + Color(red: 0.85, green: 0.85, blue: 0.85), + style: StrokeStyle(lineWidth: thickness, lineCap: .butt, lineJoin: .round) + ) + .frame(width: length, height: length) + } +} + +// ์‹œ์ž‘ ์ž‘์€ ๋ฐ•์Šค + private func makeStartRect(from rect: CGRect) -> CGRect { + let scale: CGFloat = 0.67 + let dy: CGFloat = 0 + + let w = rect.width * scale + let h = rect.height * scale + let cx = rect.midX + let cy = rect.midY + dy + + return CGRect( + x: cx - w / 2, + y: cy - h / 2, + width: w, + height: h + ) + } + +// ์ค‘๊ฐ„ ๋ฐ•์Šค +private func lerpRect(from a: CGRect, to b: CGRect, t: CGFloat) -> CGRect { + let tt = min(1, max(0, t)) + return CGRect( + x: a.origin.x + (b.origin.x - a.origin.x) * tt, + y: a.origin.y + (b.origin.y - a.origin.y) * tt, + width: a.width + (b.width - a.width) * tt, + height: a.height + (b.height - a.height) * tt + ) + } diff --git a/Shoppingmate_Frontend/View/CameraView/Roi.swift b/Shoppingmate_Frontend/View/CameraView/Roi.swift new file mode 100644 index 0000000..decb710 --- /dev/null +++ b/Shoppingmate_Frontend/View/CameraView/Roi.swift @@ -0,0 +1,30 @@ +// +// Roi.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/8/26. +// + +import CoreGraphics + +// UI ๋ฐ•์Šค/OCR ๋ฐ•์Šค ํ†ตํ•ฉ ROI +func roiRect(in size: CGSize) -> CGRect { + let w = size.width * 0.776 + let h = size.height * 0.208 + + let centerYRatio: CGFloat = 320.0 / 874.0 // ํ™”๋ฉด์˜ ์œ„์—์„œ ์•ฝ 0.366 ์ง€์  + let centerY = size.height * centerYRatio // ๋„ค๋ชจ ์ค‘์‹ฌ ์œ„์น˜ (320) + let y = centerY - (h / 2) // ๋„ค๋ชจ topY + + + return CGRect( // ๊ทธ๋ฆฌ๊ธฐ ์‹œ์ž‘ ์ง€์  + x: (size.width - w) / 2, + y: y, + width: w, + height: h + ) +} + +/// ํ™”๋ฉด ์ „์ฒด Y = 874 +/// ๋ฐ•์Šค Y ํฌ๊ธฐ = 182 +/// ๋ฐ•์Šค ์ค‘์‹ฌ = ์œ„์—์„œ 320 or ์•„๋ž˜์„œ 554 diff --git a/Shoppingmate_Frontend/View/DetailView/Card/AnalysisCard.swift b/Shoppingmate_Frontend/View/DetailView/Card/AnalysisCard.swift new file mode 100644 index 0000000..37b71b9 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/AnalysisCard.swift @@ -0,0 +1,75 @@ +// +// AnalysisCard.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/9/26. +// + +import SwiftUI + +struct AnalysisCard: View { + + let detail: DetailResponse + + private var iconNames: [String] { + AnalysisIconProvider.icons(for: detail.category) + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image("purpleBar") + .resizable() + .frame(width: 4, height: 16) + Text("5๋Œ€ ์ง€ํ‘œ ์‹ฌ์ธต ๋ถ„์„") + .font(.custom("Pretendard-Bold", size: 17)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + Spacer() + } + .padding(.bottom, 10) + + //5๊ฐœ ์ง€ํ‘œ ๋‚˜์—ด + ForEach(Array(zip(detail.indexes.indices, detail.indexes)), id: \.0) { i, item in + + HStack(alignment: .top, spacing: 12) { + + Image(iconNames[safe: i] ?? "icon_default") + .resizable() + .frame(width: 48, height: 48) + + VStack(alignment: .leading, spacing: 4) { + Text(item.name) + .font(.subheadline) + .fontWeight(.semibold) + + Text(item.reason.removingDoubleAsterisks) + .font(.caption) + .foregroundColor(.gray) + } + } + } + + + //.listRowSeparator(.hidden)//๊ตฌ๋ถ„์„  ์•ˆ๋ณด์ด๊ฒŒ + + + } + .padding(.vertical, 5) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color.white) + ) + .padding(.horizontal, 16) + } +} + +extension Array { + subscript(safe index: Int) -> Element? { + guard indices.contains(index) else { return nil } + return self[index] + } +} + +//#Preview { +// AnalysisCard() +//} diff --git a/Shoppingmate_Frontend/View/DetailView/Card/ImageCard.swift b/Shoppingmate_Frontend/View/DetailView/Card/ImageCard.swift new file mode 100644 index 0000000..ecbb57a --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/ImageCard.swift @@ -0,0 +1,47 @@ +// +// ImageCard.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/9/26. +// + +import SwiftUI + +struct ImageCard: View { + let imageUrl: String + + var body: some View { + AsyncImage(url: URL(string: imageUrl)) { phase in + switch phase { + case .empty: + ProgressView() + .frame(height: 360) + + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(height: 360) + .frame(maxWidth: .infinity) + .clipped() + .clipShape( + RoundedRectangle( + cornerRadius: 12, + style: .continuous + ) + ) + + case .failure: + Image(systemName: "photo") + .frame(height: 360) + + @unknown default: + EmptyView() + } + } + } +} +// +//#Preview { +// ImageCard() +//} diff --git a/Shoppingmate_Frontend/View/DetailView/Card/Maincard.swift b/Shoppingmate_Frontend/View/DetailView/Card/Maincard.swift new file mode 100644 index 0000000..14134bb --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/Maincard.swift @@ -0,0 +1,105 @@ +// +// Maincard.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/8/26. +// + +import SwiftUI + +struct Maincard: View { + + let detail: DetailResponse + + var body: some View { + VStack { + HStack(spacing: 12) { + Text(detail.scanName) + .font( + Font.custom("Pretendard-Bold", size: 24) + ) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + Spacer() + } //์ƒํ’ˆ๋ช… + .padding(.horizontal, 16) + .padding(.vertical, 14) + .padding(.top, 10) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color(red: 0.95, green: 0.95, blue: 1.0)) +// ) + .padding(.horizontal, 16) + Divider() + .padding(.horizontal, 30) + HStack { + Image("sparkles") + .resizable() + .frame(width: 29, height: 27) + //.padding(.leading, 5) + .padding(.trailing, -10) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.45, green: 0.35, blue: 0.95), + Color(red: 0.30, green: 0.75, blue: 0.95) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + Text("AI ํ”ฝ์Šค์ฝ”์–ด") + .font(.custom("Pretendard-Bold", size: 17)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.65, green: 0.32, blue: 0.91), + Color(red: 0.19, green: 0.53, blue: 1) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + Spacer() + StarRatingView(rating: detail.pickScore ?? 0) + Text(String(format: "%.1f", detail.pickScore ?? 0)) + .font(.custom("Pretendard-Bold", size: 35)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.45, green: 0.35, blue: 0.95), + Color(red: 0.30, green: 0.75, blue: 0.95) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + } + .padding(.horizontal, 30) + HStack { + Text("๋งˆํŠธ ํŒ๋งค๊ฐ€") + .font(.custom("Pretendard-Regular", size: 16)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + Spacer() + Text("\(detail.scanPrice, specifier: "%.0f")์›") + .font(.custom("Pretendard-Bold", size: 24)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + .padding(.leading, 5) + .padding(.bottom, 2) + } + .padding(.horizontal, 30) + HStack { + Text("์˜จ๋ผ์ธ ์ตœ์ €๊ฐ€") + .font(.custom("Pretendard-Regular", size: 16)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + Spacer() + Text("\(detail.naverPrice, specifier: "%.0f")์›") + .font(.custom("Pretendard-Bold", size: 24)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + .padding(.leading, 5) + .padding(.bottom, 2) + } + .padding(.horizontal, 30) + } //vstack + + } +} diff --git a/Shoppingmate_Frontend/View/DetailView/Card/PurchaseHoldCard.swift b/Shoppingmate_Frontend/View/DetailView/Card/PurchaseHoldCard.swift new file mode 100644 index 0000000..fca21b9 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/PurchaseHoldCard.swift @@ -0,0 +1,36 @@ +// +// PurchaseHoldCard.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/8/26. +// + +import SwiftUI + +struct PurchaseHoldCard: View { + + let detail: DetailResponse + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // ํƒ€์ดํ‹€ + Text(detail.conclusion.removingDoubleAsterisks) + .font(.custom("Pretendard-Bold", size: 20)) + .foregroundColor( + detail.conclusion.contains("๋ณด๋ฅ˜") + ? Color(red: 0.90, green: 0.30, blue: 0.35) + : Color(red: 0.25, green: 0.28, blue: 0.61) + ) + } + .padding(.vertical, 5) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color.white) + ) + .padding(.horizontal, 16) + } +} +// +//#Preview { +// PurchaseHoldCard() +//} diff --git a/Shoppingmate_Frontend/View/DetailView/Card/SaleInfoCard.swift b/Shoppingmate_Frontend/View/DetailView/Card/SaleInfoCard.swift new file mode 100644 index 0000000..9295ef4 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/SaleInfoCard.swift @@ -0,0 +1,91 @@ +// +// SaleInfoCard.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/8/26. +// + +import SwiftUI + +struct SaleInfoCard: View { + + let detail: DetailResponse + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + + // ์ƒ๋‹จ ํƒ€์ดํ‹€ + detail.scanName.contains("์˜ค๋šœ๊ธฐ ์Œ€") + ? AnyView( + HStack(spacing: 8) { + Image(systemName: "clock") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(Color(red: 0.30, green: 0.35, blue: 0.75)) + .padding(8) + .background(Color.white.opacity(0.9)) + .clipShape(Circle()) + + Text("3์ผ ๋’ค์— ํ• ์ธ ํ–‰์‚ฌ!") + .font(Font.custom("Pretendard-Bold", size: 18)) + .foregroundColor(Color(red: 0.25, green: 0.28, blue: 0.61)) + } + .padding(.top, -5) + ) + : AnyView(EmptyView()) + + // ๋‚ด์šฉ + VStack(spacing: 10) { + detail.scanName.contains("์˜ค๋šœ๊ธฐ ์Œ€") + ? AnyView( + HStack { + Text("ํ–‰์‚ฌ ๋•Œ ์‚ฌ๋ฉด") + .foregroundColor(Color(red: 0.35, green: 0.40, blue: 0.75)) + Spacer() + Text("2,500์› ๋” ์ ˆ์•ฝ") + .fontWeight(.bold) + .foregroundColor(Color(red: 0.30, green: 0.35, blue: 0.75)) + } + ) + : AnyView(EmptyView()) + + HStack { + Text( + detail.naverPrice > detail.scanPrice + ? "์˜จ๋ผ์ธ ์ตœ์ €๊ฐ€๋ณด๋‹ค" + : "์˜คํ”„๋ผ์ธ ์ตœ์ €๊ฐ€๋ณด๋‹ค" + ) + .foregroundColor(Color(red: 0.35, green: 0.40, blue: 0.75)) + Spacer() + Text( + detail.naverPrice > detail.scanPrice + ? "\(Int(detail.naverPrice - detail.scanPrice))์› ๋” ์ด๋“" + : "\(Int(detail.scanPrice - detail.naverPrice))์› ๋” ์ด๋“" + ) + .fontWeight(.bold) + .foregroundColor(Color(red: 0.30, green: 0.35, blue: 0.75)) + } + } + } + .padding(18) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color(red: 0.93, green: 0.94, blue: 1.0)) + ) + .padding(.horizontal, 5) + } +} + +//#Preview { +// let mockProduct = RecognizedProduct( +// image: UIImage(systemName: "photo"), +// badge: "Best ๊ฐ€์„ฑ๋น„", +// brand: "ํ”ผ์ฃค", +// name: "ํผ์‹ค ๋ผ๋ฒค๋” 1.5(๊ฒธ์šฉ)", +// amount: "2.5L", +// price: "8,800์›", +// onlinePrice: "12,800์›", +// perUse: "ํ•œ๋ฒˆ ์‚ฌ์šฉ 283์›๊ผด" +// ) + +// SaleInfoCard() +//} diff --git a/Shoppingmate_Frontend/View/DetailView/Card/SummaryCard.swift b/Shoppingmate_Frontend/View/DetailView/Card/SummaryCard.swift new file mode 100644 index 0000000..8a13f18 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/Card/SummaryCard.swift @@ -0,0 +1,79 @@ +// +// SummaryCard.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/8/26. +// + +import SwiftUI + +struct SummaryCard: View { + + let detail: DetailResponse + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image("purpleBar") + .resizable() + .frame(width: 4, height: 16) + Text("ํ”ฝํ”ฝ ์š”์•ฝ") + .font(.custom("Pretendard-Bold", size: 17)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + Spacer() + } + .padding(.bottom, 10) + HStack { + Image("qualityIcon") + .resizable() + .frame(width: 18, height: 18) + Text( + detail.qualityState + ? "ํ’ˆ์งˆ์€ ์šฐ์ˆ˜ํ•ด์š”" + : "ํ’ˆ์งˆ์€ ์•„์‰ฌ์›Œ์š”" + ) + .font(.custom("Pretendard-Bold", size: 15)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + } + Text(detail.qualitySummary.removingDoubleAsterisks) + .font(.custom("Pretendard-Regular", size: 14)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + .padding(.leading, 25) + .padding(.bottom, 10) + + Divider() + + HStack { + Image("priceIcon") + .resizable() + .frame(width: 18, height: 18) + Text( + detail.priceState + ? "๊ฐ€๊ฒฉ์€ ์šฐ์ˆ˜ํ•ด์š”" + : "๊ฐ€๊ฒฉ์€ ์•„์‰ฌ์›Œ์š”" + ) + .font(.custom("Pretendard-Bold", size: 15)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + } + .padding(.top, 10) + Text(detail.priceSummary.removingDoubleAsterisks) + .font(.custom("Pretendard-Regular", size: 14)) + .foregroundColor(Color(red: 0.1, green: 0.12, blue: 0.16)) + .padding(.leading, 25) + .padding(.bottom, 10) + + + + } + .padding(.vertical, 5) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color.white) + ) + .padding(.horizontal, 16) + } +} + +//#Preview { +// SummaryCard(detail: unwrappedDetail) +//} diff --git a/Shoppingmate_Frontend/View/DetailView/DetailComponents.swift b/Shoppingmate_Frontend/View/DetailView/DetailComponents.swift new file mode 100644 index 0000000..a0b0026 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/DetailComponents.swift @@ -0,0 +1,70 @@ +// +// DetailComponents.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/6/26. +// + +import SwiftUI + +struct StarRatingView: View { //๋ณ„์  UI + let rating: Double // 0~5 + + var body: some View { + HStack(spacing: 2) { + ForEach(0..<5, id: \.self) { idx in + Image(systemName: idx < Int(round(rating)) ? "star.fill" : "star") + .font(.system(size: 17)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.45, green: 0.35, blue: 0.95), + Color(red: 0.30, green: 0.75, blue: 0.95) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + } + } + } +} + +struct PriceRow: View { // ๊ฐ€๊ฒฉ ํ‘œ์‹œ ์ค„ + let title: String + let price: String + let isEmphasis: Bool + + var body: some View { + HStack(alignment: .firstTextBaseline) { + Text(title) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.black.opacity(0.75)) + + Spacer() + + Text(price) + .font(.system(size: 16, weight: isEmphasis ? .bold : .semibold)) + .foregroundStyle(isEmphasis ? .red : .black) + } + } +} + +struct InfoCard: View { // ์„น์…˜ ์นด๋“œ ์ปจํ…Œ์ด๋„ˆ + let title: String + @ViewBuilder var content: Content + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(.black) + + content + } + .padding(14) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .shadow(color: .black.opacity(0.06), radius: 10, x: 0, y: 4) + } +} diff --git a/Shoppingmate_Frontend/View/DetailView/DetailView.swift b/Shoppingmate_Frontend/View/DetailView/DetailView.swift new file mode 100644 index 0000000..d90ad13 --- /dev/null +++ b/Shoppingmate_Frontend/View/DetailView/DetailView.swift @@ -0,0 +1,144 @@ +// +// DetailView.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/6/26. + +import SwiftUI + +struct DetailView: View { + + @Environment(\.dismiss) private var dismiss + + let scanId: Int + + @State private var detail: DetailResponse? + + init(scanId: Int, previewDetail: DetailResponse? = nil) { + self.scanId = scanId + self._detail = State(initialValue: previewDetail) + } + + var body: some View { + ZStack{ + Color(red: 0.95, green: 0.95, blue: 0.95) + .ignoresSafeArea(edges: .all) + + VStack(spacing:0) { + ZStack { + Rectangle() + .frame(height: 61) + .foregroundColor(.white) + HStack { + Button { + dismiss() + } label: { + Image("backArrow") + .resizable() + .frame(width: 20, height: 24) + .padding(.leading, 20) + } + Text("ํ”ฝ์Šค์ฝ”์–ด") + .foregroundColor(Color.black) + .font( + Font.custom("Pretendard-Bold", size: 20) + ) + Spacer() + + NavigationLink { + CameraOCRView(cameFromMap: true) + } label: { + Image("cameraBack") + .resizable() + .frame(width: 35, height: 35) + .padding(.trailing, 10) + } + + } + } + Divider() + + if let unwrappedDetail = detail { +// if let imageUrl = unwrappedDetail.naverImage { +// ImageCard(imageUrl: imageUrl) +// .padding(.horizontal,17) +// .padding(.top,20) +// } + List{ + Section { + if let imageUrl = unwrappedDetail.naverImage, + imageUrl.hasPrefix("http") { + + ImageCard(imageUrl: imageUrl) + .listRowInsets( + EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16) + ) + + } else { + HStack { + Spacer() + Image(systemName: "photo") + .resizable() + .scaledToFit() + .frame(height: 200) + .foregroundColor(.gray.opacity(0.4)) + Spacer() + } + .listRowInsets( + EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16) + ) + } + } + .listSectionSpacing(20) + + Section { + Maincard(detail: unwrappedDetail) + .listRowInsets(EdgeInsets()) + SaleInfoCard(detail: unwrappedDetail) + .listRowSeparator(.hidden) + } + //๊ตฌ๋งค ์ถ”์ฒœ or ๋น„์ถ”์ฒœ + Section { + PurchaseHoldCard(detail: unwrappedDetail) + } + //ํ’ˆ์งˆ ๋ฐ ๊ฐ€๊ฒฉ ์š”์•ฝ + Section { + SummaryCard(detail: unwrappedDetail) + } + //5๋Œ€ ์ง€ํ‘œ ์‹ฌ์ธต ๋ถ„์„ + Section { + AnalysisCard(detail: unwrappedDetail) + } + } + .listSectionSpacing(18) + //.listStyle(.plain) + } else { + Spacer() + ProgressView() + Spacer() + } + + } //vstack + } // zstack all + .navigationBarHidden(true) + //๋น„๋™๊ธฐ ํ˜ธ์ถœ + .task { + await loadDetail() + } + } + private func loadDetail() async { + do { + detail = try await getGemini(scanId: scanId) + } catch { + print("โŒ detail ๋กœ๋”ฉ ์‹คํŒจ:", error) + } + } +} + + + +#Preview { + NavigationStack { + DetailView(scanId: 1) + } +} diff --git a/Shoppingmate_Frontend/View/LoginView.swift b/Shoppingmate_Frontend/View/LoginView.swift new file mode 100644 index 0000000..15f8c96 --- /dev/null +++ b/Shoppingmate_Frontend/View/LoginView.swift @@ -0,0 +1,93 @@ +// +// LoginView.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 12/30/25. +// + +import SwiftUI +import Combine + +enum UserType { + case normal + case partner +} + +struct LoginView: View { + @State private var goToLocation = false + @EnvironmentObject var loginViewModel: LoginViewModel + @EnvironmentObject var serverViewModel: ServerViewModel + + var body: some View { +// NavigationStack { + ZStack { + Color(red: 0.98, green: 0.98, blue: 0.98) + .ignoresSafeArea(edges: .all) + VStack(alignment: .leading) { + Spacer() + Image("PICPICK") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 147, height: 25) + .clipped() + .padding(.bottom, 25) + Text("์ดˆ๊ฐ„๋‹จ ์˜จ/์˜คํ”„๋ผ์ธ ๊ฐ€๊ฒฉ๋น„ํ‘œ") + .font(.custom("Pretendard-Regular", size: 26)) + .multilineTextAlignment(.center) + .foregroundColor(.black) + .padding(.bottom, 15) + Text("์ง€๊ธˆ ๋ฐ”๋กœ,\n์‹œ์ž‘ํ•ด ๋ณผ๊นŒ์š”?") + .font( + Font.custom("Pretendard-Bold", size: 43) + ) + .foregroundColor(.black) + .padding(.bottom, 160) + + VStack { + //๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ + HStack(alignment: .center, spacing: 8) { + Button { + loginViewModel.guestLogin() +// goToLocation = true + //appState.userType = .normal + } label: { + HStack(alignment: .center, spacing: 8) { + Text("๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ") + .font( + Font.custom("Pretendard-Bold", size: 16) + ) + .foregroundColor(.white) + } + .frame(width: 362, height: 55, alignment: .center) + .background(Color(red: 0.25, green: 0.28, blue: 0.61)) + .cornerRadius(12) + } + .onChange(of: loginViewModel.isUserReady) { ready in + if ready { + //์œ„์น˜ ๋“ค์–ด์˜จ ๋’ค ํ™•์ธ ํ›„ ์ฒ˜๋ฆฌ + serverViewModel.handleLocationAfterLogin() + goToLocation = true + } + } + .buttonStyle(.plain) + } //hstack + .padding(.bottom,26) + .padding(.top, 150) + + }//๋ฒ„ํŠผ VStack + + //Spacer() + }//vstack + }//zstack + .navigationDestination(isPresented: $goToLocation) { + let uid = UserDefaults.standard.integer(forKey: "userId") + LocationSelectView(userIdResponse: UserIdResponse(userId: uid)) +// LocationSelectView() + } + } +} + +// +//#Preview { +// LoginView() +//} diff --git a/Shoppingmate_Frontend/View/MapView/CurrentLocationButton.swift b/Shoppingmate_Frontend/View/MapView/CurrentLocationButton.swift new file mode 100644 index 0000000..e56f489 --- /dev/null +++ b/Shoppingmate_Frontend/View/MapView/CurrentLocationButton.swift @@ -0,0 +1,35 @@ +// +// CurrentLocationButton.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/6/26. +// + +import SwiftUI + +//@StateObject private var viewModel: LocationSelectViewModel + +struct CurrentLocationButton: View { + let onTap: () -> Void + + var body: some View { + Button { + onTap() + } label: { + Image("currentLocation") + .resizable() + .frame(width: 22, height: 22) + .padding(12) + .frame(width: 46, height: 46) + .background(Color.white) + .cornerRadius(23) + .shadow(color: .black.opacity(0.25), radius: 1.5) + } + } +} + +#Preview { + CurrentLocationButton { + print("Tapped") + } +} diff --git a/Shoppingmate_Frontend/View/MapView/LocationBottomSheet.swift b/Shoppingmate_Frontend/View/MapView/LocationBottomSheet.swift new file mode 100644 index 0000000..443b5b9 --- /dev/null +++ b/Shoppingmate_Frontend/View/MapView/LocationBottomSheet.swift @@ -0,0 +1,97 @@ +// +// LocationBottomSheet.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/5/26. +// + +import SwiftUI + +struct LocationBottomSheet: View { + + @ObservedObject var viewModel: LocationSelectViewModel + + //let address: String// ํ˜„์žฌ ์„ ํƒ๋œ ์ฃผ์†Œ + let onCurrentLocationTap: () -> Void// "๋‹ค๋ฅธ ์œ„์น˜" ๋ˆŒ๋ €์„ ๋•Œ + let onConfirmTap: () -> Void// "์ด ์œ„์น˜๊ฐ€ ๋งž์•„์š”" ๋ˆŒ๋ €์„ ๋•Œ + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + // ์ฃผ์†Œ ์˜์—ญ + VStack(alignment: .leading, spacing: 8) { + Text("ํ˜„์žฌ ์œ„์น˜") + .font(.custom("Pretendard-Regular", size: 14)) + .foregroundColor(Color(red: 0.42, green: 0.45, blue: 0.51)) + Text("ํ”ฝํ”ฝ ๋งˆํŠธ") + .font( + Font.custom("Pretendard-Bold", size: 22) + ) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + if let address = viewModel.address { + Text(address) + .font(.custom("Pretendard-Regular", size: 15)) + .foregroundColor(Color(red: 0.29, green: 0.33, blue: 0.4)) + } else { + Text("์ฃผ์†Œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ") + .font(.custom("Pretendard-Regular", size: 15)) + .foregroundColor(.gray) + } + } + .padding(.horizontal, 23) + .padding(.top, 23) + + //"์ด ์œ„์น˜๋กœ ์ฃผ์†Œ ๋“ฑ๋ก" ๋ฒ„ํŠผ + Button { + onConfirmTap() + } label: { + Text("์ด ์œ„์น˜๋กœ ์„ค์ •") + .font( + Font.custom("Pretendard-Bold", size: 16) + ) + .foregroundColor(.white) + .frame(width: 360, height: 52) + .background(Color(red: 0.25, green: 0.28, blue: 0.61)) + .cornerRadius(14) + } + //.padding(.horizontal, 55) + //.padding(.vertical, 13) + .padding(.horizontal, 20) + .padding(.top, 16) + } + .padding(.horizontal, 0) + .padding(.bottom, 40) + .frame(maxWidth: .infinity) + .background(Color.white) + .clipShape(TopRoundedRect(radius: 24)) + .ignoresSafeArea(edges: .bottom) + } +} + +//#Preview { +// LocationBottomSheet( +// viewModel: address, +// onCurrentLocationTap: { +// print("ํ˜„์žฌ ์œ„์น˜") +// }, +// onConfirmTap: { +// print("ํ™•์ •") +// } +// ) +//} +#Preview { + let service = LocationService() + let vm = LocationSelectViewModel(locationService: service) + + // ์ฃผ์†Œ๋ฅผ ๋ฏธ๋ฆฌ ๋ณด์ด๊ฒŒ ํ•˜๊ณ  ์‹ถ์œผ๋ฉด (address๊ฐ€ set ๊ฐ€๋Šฅํ•  ๋•Œ) + vm.address = "๊ฒฝ๋ถ ํฌํ•ญ์‹œ ๋ถ๊ตฌ ํฅํ•ด์ ํ•œ๋™๋กœ 558" + + return LocationBottomSheet( + viewModel: vm, + onCurrentLocationTap: { + print("ํ˜„์žฌ ์œ„์น˜") + }, + onConfirmTap: { + print("ํ™•์ •") + } + ) +} diff --git a/Shoppingmate_Frontend/View/MapView/LocationSelectView.swift b/Shoppingmate_Frontend/View/MapView/LocationSelectView.swift new file mode 100644 index 0000000..2f594c0 --- /dev/null +++ b/Shoppingmate_Frontend/View/MapView/LocationSelectView.swift @@ -0,0 +1,117 @@ +// +// LocationSelectView.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/5/26. +// + +import SwiftUI +import MapKit + +struct LocationSelectView: View { + @EnvironmentObject private var serverViewModel: ServerViewModel + + @StateObject private var locationService = LocationService() + @StateObject private var viewModel: LocationSelectViewModel + + let userIdResponse: UserIdResponse + + //private let currentLocationButtonView = CurrentLocationButton() + + // LocationService๋ฅผ ViewModel๊ณผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•œ init +// init() { +// let service = LocationService() +// +// _locationService = StateObject(wrappedValue: service) +// _viewModel = StateObject( +// wrappedValue: LocationSelectViewModel(locationService: service) +// ) +// } + init(userIdResponse: UserIdResponse) { + self.userIdResponse = userIdResponse + + let service = LocationService() + _locationService = StateObject(wrappedValue: service) + _viewModel = StateObject( + wrappedValue: LocationSelectViewModel(locationService: service) + ) + } + + + var body: some View { + ZStack { + // Apple Map + Map(coordinateRegion: $viewModel.region) + .ignoresSafeArea(edges: .all) + VStack{ + Spacer() + Rectangle() + .fill(Color.white) + .frame(height: 100) + .frame(maxWidth: .infinity) + } + .ignoresSafeArea(edges: .bottom) + + VStack { + Image("bubble") + .resizable() + .frame(width: 120, height: 60) + // ์ค‘์•™ ๊ณ ์ • ํ•€(์ง€๋„๋Š” ์›€์ง์ด๊ณ  ํ•€์€ ๊ณ ์ •) + Image("mapPin") + .resizable() + .frame(width: 48, height: 56) + .offset(y: -18) + }//vstack + .padding(.bottom, 150) + } + .navigationBarBackButtonHidden(true) + + // BottomSheet + .overlay(alignment: .bottom) { + LocationBottomSheet( + viewModel: viewModel, + // ๋‹ค๋ฅธ ์œ„์น˜ + onCurrentLocationTap: { + viewModel.moveToCurrentLocation() + serverViewModel.handleLocationUpdateAfterButton() + }, + //์ด ์œ„์น˜๋กœ ์„ค์ • + onConfirmTap: { + viewModel.confirmLocation() + } + ) +// .ignoresSafeArea(edges: .bottom) + } + .overlay(alignment: .bottomTrailing) { + CurrentLocationButton { + print("๐Ÿ“Œ ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ ๋ˆŒ๋ฆผ") + viewModel.moveToCurrentLocation() + } + .padding(Edge.Set.trailing, 20) + .padding(Edge.Set.bottom, 230) // BottomSheet ๋†’์ด๋งŒํผ ๋„์šฐ๊ธฐ + } + // ๋‹ค์Œ ํ™”๋ฉด ์ด๋™ + .navigationDestination( + isPresented: $viewModel.isConfirmed + ) { + CameraOCRView(cameFromMap: true, userIdResponse: userIdResponse) } + } +} +// +//#Preview { +// NavigationStack{ +// LocationSelectView(userIdResponse: UserIdResponse(userId: 1)) +// } +// .environmentObject(ServerViewModel()) +//} + +#Preview { + let loginVM = LoginViewModel() + let serverVM = ServerViewModel(loginViewModel: loginVM) + + return NavigationStack { + LocationSelectView(userIdResponse: UserIdResponse(userId: 1)) + } + .environmentObject(loginVM) // ํ•„์š”ํ•˜๋ฉด ๊ฐ™์ด ์ฃผ์ž… + .environmentObject(serverVM) +} diff --git a/Shoppingmate_Frontend/View/MapView/TopRoundedRect.swift b/Shoppingmate_Frontend/View/MapView/TopRoundedRect.swift new file mode 100644 index 0000000..92a2973 --- /dev/null +++ b/Shoppingmate_Frontend/View/MapView/TopRoundedRect.swift @@ -0,0 +1,21 @@ +// +// TopRoundedRect.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/9/26. +// + +import SwiftUI + +struct TopRoundedRect: Shape { + var radius: CGFloat = 24 + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: [.topLeft, .topRight], + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} diff --git a/Shoppingmate_Frontend/View/MapView/View+CornerRadius.swift b/Shoppingmate_Frontend/View/MapView/View+CornerRadius.swift new file mode 100644 index 0000000..e56a4b8 --- /dev/null +++ b/Shoppingmate_Frontend/View/MapView/View+CornerRadius.swift @@ -0,0 +1,40 @@ +// +// View+CornerRadius.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/7/26. +// + +import SwiftUI +// +//struct RoundedCorner: Shape { +// var radius: CGFloat = .infinity +// var corners: UIRectCorner = .allCorners +// +// func path(in rect: CGRect) -> Path { +// let path = UIBezierPath( +// roundedRect: rect, +// byRoundingCorners: corners, +// cornerRadii: CGSize(width: radius, height: radius) +// ) +// return Path(path.cgPath) +// } +//} +// +//extension View { +// func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { +// clipShape(RoundedCorner(radius: radius, corners: corners)) +// } +//} +struct TopRoundedRectangle: Shape { + var radius: CGFloat = 20 + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: [.topLeft, .topRight], // ๐Ÿ‘ˆ ์œ„์ชฝ๋งŒ + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} diff --git a/Shoppingmate_Frontend/View/OCRResultView/BottomRoundedRectangle.swift b/Shoppingmate_Frontend/View/OCRResultView/BottomRoundedRectangle.swift new file mode 100644 index 0000000..30985a6 --- /dev/null +++ b/Shoppingmate_Frontend/View/OCRResultView/BottomRoundedRectangle.swift @@ -0,0 +1,25 @@ +// +// BottomRoundedRectangle.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/8/26. +// + +import SwiftUI + +struct BottomRoundedRectangle: Shape { + var radius: CGFloat = 16 + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: [.bottomLeft, .bottomRight], + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +#Preview { + BottomRoundedRectangle() +} diff --git a/Shoppingmate_Frontend/View/OCRResultView/ProductCardView.swift b/Shoppingmate_Frontend/View/OCRResultView/ProductCardView.swift new file mode 100644 index 0000000..bd3d7c6 --- /dev/null +++ b/Shoppingmate_Frontend/View/OCRResultView/ProductCardView.swift @@ -0,0 +1,161 @@ +// +// ProductCardView.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/2/26. +// + +import SwiftUI + +// ํ”ฝ๋‹จ๊ฐ€ ํŽ˜์ด์ง€ components +struct ProductCardView: View { + let product: RecognizedProduct + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + + ZStack(alignment: .bottomLeading) { + + GeometryReader { geo in + let side = geo.size.width + + if let urlString = product.imageURL, + let url = URL(string: urlString) { + + AsyncImage(url: url) { phase in + switch phase { + case .empty: + RoundedRectangle(cornerRadius: 9) + .fill(Color(red: 0.91, green: 0.91, blue: 0.91)) + .overlay(ProgressView()) + .frame(width: side, height: side) + + case .success(let image): + image + .resizable() + .scaledToFit() + .frame(width: side, height: side) +// .clipped() + .background( + RoundedRectangle(cornerRadius: 9) + .fill(Color(red: 0.91, green: 0.91, blue: 0.91)) + ) + .clipShape(RoundedRectangle(cornerRadius: 9, style: .continuous)) + + case .failure: + RoundedRectangle(cornerRadius: 9) + .fill(Color(red: 0.91, green: 0.91, blue: 0.91)) + .overlay(Image(systemName: "photo").font(.system(size: 24))) + .frame(width: side, height: side) + + @unknown default: + RoundedRectangle(cornerRadius: 9) + .fill(Color(red: 0.91, green: 0.91, blue: 0.91)) + .frame(width: side, height: side) + } + } + + } else { + RoundedRectangle(cornerRadius: 9) + .fill(Color(red: 0.91, green: 0.91, blue: 0.91)) + .overlay(Image(systemName: "photo").font(.system(size: 24))) + .frame(width: side, height: side) + } + } + .aspectRatio(1, contentMode: .fit) // ์ •์‚ฌ๊ฐํ˜• ์œ ์ง€ + + Text(product.perUse) + .font( + Font.custom("Pretendard-Bold", size: 11) + ) + .foregroundStyle( + LinearGradient( + stops: [ + .init(color: Color(red: 0.65, green: 0.32, blue: 0.91), location: 0.00), + .init(color: Color(red: 0.19, green: 0.53, blue: 1.00), location: 0.53), + .init(color: Color(red: 0.04, green: 0.83, blue: 0.73), location: 1.00), + ], + startPoint: UnitPoint(x: -0.01, y: 0.49), + endPoint: UnitPoint(x: 1.00, y: 0.49) + ) + ) + .padding(.horizontal, 6) + .padding(.vertical, 6) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 4)) +// .padding(.top, 6) +// .padding(.leading, 6) + .overlay( + RoundedRectangle(cornerRadius: 4) + //.inset(by: 0.65) + .stroke( + LinearGradient( + stops: [ + Gradient.Stop(color: Color(red: 0.65, green: 0.32, blue: 0.91), location: 0.00), + Gradient.Stop(color: Color(red: 0.19, green: 0.53, blue: 1), location: 0.53), + Gradient.Stop(color: Color(red: 0.04, green: 0.83, blue: 0.73), location: 1.00), + ], + startPoint: UnitPoint(x: -0.01, y: 0.49), + endPoint: UnitPoint(x: 1, y: 0.49) + ), + lineWidth: 1.3) + + ) + .padding(.horizontal, 6) + .padding(.vertical, 6) + + + + } // ZStack badge + + VStack(alignment: .leading, spacing: 4) { + Text(product.brand) + .font(.custom("Pretendard-Regular", size: 11)) + .foregroundColor(Color(red: 0.59, green: 0.59, blue: 0.59)) + + Text(product.name) + .font(.custom("Pretendard-Regular", size: 12)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + .frame(width: 165, height:31, alignment: .topLeading) + +// Divider() + + HStack { + Text("๋งˆํŠธ ํŒ๋งค๊ฐ€") + .font(.custom("Pretendard-Bold", size: 10)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + Spacer() + let price = product.price.digitsInt + Text(price.map { "\($0.formatted(.number))์›" } ?? "-") + .font(.custom("Pretendard-Bold", size: 18)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + .padding(.leading, 5) + .padding(.bottom, 2) + } + HStack { + Text("์˜จ๋ผ์ธ ์ตœ์ €๊ฐ€") + .font(.custom("Pretendard-Bold", size: 10)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + Spacer() + let price = product.onlinePrice.digitsInt + Text(price.map { "\($0.formatted(.number))์›" } ?? "-") + // if let price = Int(product.onlinePrice) { + // Text("\(price.formatted(.number))์›") + .font(.custom("Pretendard-Bold", size: 18)) + .foregroundColor(Color(red: 0.06, green: 0.09, blue: 0.16)) + .padding(.leading, 5) + .padding(.bottom, 2) +// } + } + } // VStack text + Spacer() // ์ œ๋ชฉ 2์ค„ + } // VStack all + } +} + +extension String { + var digitsInt: Int? { + let digits = self.filter { $0.isNumber } + return Int(digits) + } +} diff --git a/Shoppingmate_Frontend/View/OCRResultView/RecognitionResultView.swift b/Shoppingmate_Frontend/View/OCRResultView/RecognitionResultView.swift new file mode 100644 index 0000000..19ce063 --- /dev/null +++ b/Shoppingmate_Frontend/View/OCRResultView/RecognitionResultView.swift @@ -0,0 +1,138 @@ +// +// RecognitionResultView.swift +// Shoppingmate_Frontend +// +// Created by Jinsoo Park on 1/1/26. +// + +import SwiftUI +import UIKit + +// ํ”ฝ๋‹จ๊ฐ€ ํŽ˜์ด์ง€ +struct RecognitionResultView: View { + + @Environment(\.dismiss) private var dismiss // ์ปค์Šคํ…€ ๋’ค๋กœ๊ฐ€๊ธฐ + + let products: [RecognizedProduct] + let userId: Int? + + private let columns = [ //2ํ–‰ ์ •๋ ฌ + GridItem(.flexible(), spacing: 14), + GridItem(.flexible(), spacing: 14) + ] + + + private var productCountText: String { + "\(products.count)๊ฐœ ์ƒํ’ˆ" + } + + var body: some View { + ZStack{ + Color(red: 0.95, green: 0.95, blue: 0.95) + .ignoresSafeArea(edges: .all) + VStack { + ZStack { + Rectangle() + .frame(height: 61) + .foregroundColor(.white) + HStack { + Button { + dismiss() + } label: { + Image("backArrow") + .resizable() + .frame(width: 20, height: 24) + .padding(.leading, 20) + } + Text("ํ”ฝ๋‹จ๊ฐ€") + .foregroundColor(Color.black) + .font(.custom("Pretendard-Bold", size: 20)) + Spacer() + } + } + Divider() + .padding(.top, -12) + HStack(spacing: 12) { + Image("sparkles") + .resizable() + .frame(width: 29, height: 27) + //.padding(.leading, 5) + .padding(.trailing, -10) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.45, green: 0.35, blue: 0.95), + Color(red: 0.30, green: 0.75, blue: 0.95) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + Text("Ai ํ”ฝ๋‹จ๊ฐ€") + .font(.custom("Pretendard-Bold", size: 17)) + .foregroundStyle( + LinearGradient( + colors: [ + Color(red: 0.65, green: 0.32, blue: 0.91), + Color(red: 0.19, green: 0.53, blue: 1) + ], + startPoint: .leading, + endPoint: .trailing + ) + ) + Text("ํ™˜์‚ฐ์œผ๋กœ ์ตœ์ €๊ฐ€๋ฅผ ํ™•์ธํ•˜์„ธ์š”") + .font(.custom("Pretendard-Bold", size: 17)) + .foregroundColor(Color(red: 0.25, green: 0.28, blue: 0.61)) + .lineLimit(1) + .padding(.leading, -8) + }//hstack + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background( + BottomRoundedRectangle(radius: 9) + .fill(Color(red: 0.89, green: 0.9, blue: 1)) + .frame(width: 349, height: 61) + ) + .padding(.horizontal, 16) + .padding(.top, -17) + + ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(products) { product in + NavigationLink { + DetailView(scanId: product.scanId) + } label: { + ProductCardView(product: product) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 21) + .padding(.top, 30) + } //ScrollView + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + } + } //zstack + + } +} + +//#Preview { +// let mockProducts: [RecognizedProduct] = [ +// RecognizedProduct( +// image: UIImage(systemName: "photo"), +// badge: "Best ๊ฐ€์„ฑ๋น„", +// brand: "ํ”ผ์ฃค", +// name: "ํ”ผ์ฃค ์‹ค๋‚ด๊ฑด์กฐ ์„ฌ์œ ์œ ์—ฐ์ œ ๋ผ๋ฒค๋”ํ–ฅ", +// amount: "2.5L", +// price: "8,800์›", +// onlinePrice: "12,800์›", +// perUse: "ํ•œ๋ฒˆ ์‚ฌ์šฉ 283์›๊ผด", +// scanId: 12345 +// ) +// ] +// NavigationStack { +// RecognitionResultView(products: mockProducts) +// } +//} diff --git a/Shoppingmate_Frontend/View/OnboardingLogoView.swift b/Shoppingmate_Frontend/View/OnboardingLogoView.swift new file mode 100644 index 0000000..2f0b0b1 --- /dev/null +++ b/Shoppingmate_Frontend/View/OnboardingLogoView.swift @@ -0,0 +1,38 @@ +// +// OnboardingLogoView.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/5/26. +// + +import SwiftUI +import AVKit + +struct OnboardingLogoView: View { + + let onFinished: () -> Void + + private let player = AVPlayer( + url: Bundle.main.url(forResource: "logo", withExtension: "mp4")! + ) + + var body: some View { + VideoPlayer(player: player) + .onAppear { + player.isMuted = true + player.play() + + NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: player.currentItem, + queue: .main + ) { _ in + onFinished() + } + } + .onDisappear { + player.pause() + NotificationCenter.default.removeObserver(self) + } + } +} diff --git a/Shoppingmate_Frontend/View/OnboardingView.swift b/Shoppingmate_Frontend/View/OnboardingView.swift new file mode 100644 index 0000000..aca606f --- /dev/null +++ b/Shoppingmate_Frontend/View/OnboardingView.swift @@ -0,0 +1,111 @@ +// +// OnboardingView.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 12/30/25. +// + +import SwiftUI + +struct OnboardingView: View { + + @State private var isFinished = false + @State private var hasUUID = false + @State private var didUploadUUID = false + + @State private var userIdResponse: UserIdResponse? = nil //uploadUUID ๊ฒฐ๊ณผ ๋„˜๊ธฐ๊ธฐ + + @EnvironmentObject var loginViewModel: LoginViewModel + @EnvironmentObject var serverViewModel: ServerViewModel + private let uploadService = UploadService() + + var body: some View { + Group { + if !isFinished { + ZStack { + Color(red: 65/255, green: 71/255, blue: 155/255) + .ignoresSafeArea() + + OnboardingLogoView { + isFinished = true + } + .frame(width: 420, height: 840) + } + } else if hasUUID { + + + // โœ… userIdResponse๊ฐ€ ์ค€๋น„๋˜๊ธฐ ์ „๊นŒ์ง€๋Š” "์•„๋ฌด ๋ฌธ๊ตฌ ์—†์ด" ๋นˆ ๋ฐฐ๊ฒฝ๋งŒ ๋ณด์—ฌ์คŒ + if let userIdResponse { + CameraOCRView(cameFromMap: false, userIdResponse: userIdResponse) + } else { + Color(red: 65/255, green: 71/255, blue: 155/255) + .ignoresSafeArea() + } +// CameraOCRView(cameFromMap: false) + + + } else { + LoginView() + } + } + .onAppear { + checkUUID() + } + } + + private func checkUUID() { + guard let uuid = UserDefaults.standard.string( + forKey: LoginViewModel.UserDefaultKey.uuid + ) else { + hasUUID = false + print("๐Ÿ†• UUID ์—†์Œ โ†’ LoginView ์ด๋™") + return + } + + hasUUID = true + print("๐Ÿ†” ๊ธฐ์กด UUID:", uuid) + + // ๊ธฐ์กด UUID๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ POST + if !didUploadUUID { + didUploadUUID = true + + let uuidDTO = UUIDDTO(uuid: uuid) + + Task { + do { + let decoded = try await uploadService.uploadUUID(uuid: uuidDTO) + print("โœ… ๊ธฐ์กด UUID ์„œ๋ฒ„ ์ „์†ก ์™„๋ฃŒ") + + await MainActor.run { + self.userIdResponse = decoded + serverViewModel.handleLocationAfterLogin() + } +// serverViewModel.handleLocationAfterLogin() + + } catch { + print("๐Ÿšจ ๊ธฐ์กด UUID ์„œ๋ฒ„ ์ „์†ก ์‹คํŒจ:", error) + + await MainActor.run { + self.didUploadUUID = false + self.userIdResponse = nil + } + } + } +// +// Task { +// do { +// try await uploadService.uploadUUID(uuid: uuidDTO) +// print("โœ… ๊ธฐ์กด UUID ์„œ๋ฒ„ ์ „์†ก ์™„๋ฃŒ") +// +// serverViewModel.handleLocationAfterLogin() +// } catch { +// print("๐Ÿšจ ๊ธฐ์กด UUID ์„œ๋ฒ„ ์ „์†ก ์‹คํŒจ:", error) +// } +// } + } + } +} + +#Preview { + OnboardingView() +} diff --git a/Shoppingmate_Frontend/View/ROIOverlay.swift b/Shoppingmate_Frontend/View/ROIOverlay.swift deleted file mode 100644 index d4215dc..0000000 --- a/Shoppingmate_Frontend/View/ROIOverlay.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ROIOverlay.swift -// Shoppingmate_Frontend -// -// Created by Jinsoo Park on 12/26/25. -// - -import SwiftUI - -struct ROIOverlay: View { - - let onUpdate: (CGRect) -> Void - - var body: some View { - GeometryReader { geo in - let width = geo.size.width * 0.8 - let height = geo.size.height * 0.2 - - let rect = CGRect( - x: (geo.size.width - width) / 2, - y: (geo.size.height - height) / 2, - width: width, - height: height - ) - - // ๊ฐ€์ด๋“œ ๋ฐ•์Šค - RoundedRectangle(cornerRadius: 12) - .stroke(Color.yellow, lineWidth: 3) - .frame(width: width, height: height) - .position(x: geo.size.width / 2, - y: geo.size.height / 2) - .onAppear { - onUpdate(rect) - } -// .onChange(of: geo.size) { _ in onUpdate(rect) } // โœ… ํฌ๊ธฐ ๋ฐ”๋€Œ๋ฉด ๊ฐฑ์‹  - } - .ignoresSafeArea() - } -} diff --git a/Shoppingmate_Frontend/ViewModel/CameraManager.swift b/Shoppingmate_Frontend/ViewModel/CameraManager.swift index 111291b..dc7b859 100644 --- a/Shoppingmate_Frontend/ViewModel/CameraManager.swift +++ b/Shoppingmate_Frontend/ViewModel/CameraManager.swift @@ -3,51 +3,45 @@ // Shoppingmate_Frontend // // Created by Jinsoo Park on 12/26/25. -// + import AVFoundation import Vision import UIKit -import SwiftUI import Combine//@Published (ObservableObject์šฉ) -import CoreLocation -@MainActor//์ด ํด๋ž˜์Šค์˜ ๊ธฐ๋ณธ ์‹คํ–‰ ์ปจํ…์ŠคํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ(UI ์ƒํƒœ(@Published) ์•ˆ์ „) -//NSObject: AVCapturePhotoCaptureDelegate๋ฅผ ์“ฐ๊ธฐ ์œ„ํ•ด ํ•„์š” +//@MainActor final class CameraManager: NSObject, ObservableObject { - private let locationService = LocationService() private let uploadService = UploadService() - private var capturedLocation: CLLocation? - + private var isConfigured = false //์นด๋ฉ”๋ผ ์ตœ์ดˆ ์„ธํŒ… ์™„๋ฃŒ ์—ฌ๋ถ€ + // SwiftUI์—์„œ ๊ด€์ฐฐํ•  ์ƒํƒœ - @Published var recognizedText: String = ""//OCR ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด - @Published var isProcessing = false//OCR ์ค‘์ธ์ง€ ์—ฌ๋ถ€(๋กœ๋”ฉ UI์šฉ) - - // ์นด๋ฉ”๋ผ ์„ธ์…˜ (์—”์ง„์˜ ์ค‘์‹ฌ) + @Published var recognizedText: String = "" //OCR ๊ฒฐ๊ณผ ์ถœ๋ ฅ(์šฉ) + @Published var isProcessing = false //OCR ๋กœ๋”ฉ ์ค‘ +// @Published var croppedROIImage: UIImage? = nil // ROI๋กœ ์ž˜๋ฆฐ ์ด๋ฏธ์ง€ ์ €์žฅ ๋ณ€์ˆ˜ (์ฐ์œผ๋ฉด ์—…๋ฐ์ดํŠธ) + + @Published var capturedROIImages: [UIImage] = [] // ์‚ฌ์ง„ ์—ฌ๋Ÿฌ ์žฅ ๋ˆ„์  ์ €์žฅ +// @Published var capturedTexts: [String] = [] // OCR ๋ˆ„์  + @Published var OCRFilters: [OCRFilter] = [] // OCR ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ํ•„ํ„ฐ + + // ์นด๋ฉ”๋ผ ์„ธ์…˜ let session = AVCaptureSession() private let photoOutput = AVCapturePhotoOutput()//photoOutput: ์‹ค์ œ ์‚ฌ์ง„ ์ดฌ์˜ ๋‹ด๋‹น - - // ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด (์ขŒํ‘œ ๋ณ€ํ™˜์šฉ) - // - SwiftUI CameraPreview(UIViewRepresentable)์—์„œ ์ƒ์„ฑ๋œ previewLayer๋ฅผ ์—ฌ๊ธฐ๋กœ ์ฃผ์ž…ํ•ด์•ผ ํ•จ - //์นด๋ฉ”๋ผ ํ™”๋ฉด์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ ˆ์ด์–ด - var previewLayer: AVCaptureVideoPreviewLayer? - - // SwiftUI์—์„œ ๊ณ„์‚ฐํ•œ ROI (previewLayer ์ขŒํ‘œ๊ณ„) - // - ROIOverlay์—์„œ ๊ณ„์‚ฐํ•œ CGRect๋ฅผ updateROIRect๋กœ ๊ณ„์† ๋„ฃ์–ด์คŒ - // vision์—์„œ ์“ฐ๊ธฐ ์ „์— ์ขŒํ‘œ๊ณ„ ๋ณ€ํ™˜๋จ - fileprivate var roiLayerRect: CGRect = .zero - + private let sessionQueue = DispatchQueue(label: "camera.session.queue") // ์„ธ์…˜ ์ œ์–ด ์ „์šฉ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋งŒ๋“ค๊ธฐ + + var previewLayer: AVCaptureVideoPreviewLayer? // ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด + // MARK: - Session ์„ค์ • - func startSession() { - if session.isRunning { return }//์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ + func configureSession() { // ์นด๋ฉ”๋ผ ์„ธ์…˜ ์ตœ์ดˆ ์„ธํŒ… 1ํšŒ ํ›„ ์œ ์ง€ + guard !isConfigured else { return } //์„ธํŒ…์ด ๋˜์–ด์žˆ์„ ์‹œ return - session.beginConfiguration()// ์นด๋ฉ”๋ผ ์„ค์ • ์‹œ์ž‘ + session.beginConfiguration() // ์นด๋ฉ”๋ผ ์„ค์ • ์‹œ์ž‘ session.sessionPreset = .photo //์‚ฌ์ง„ ์ดฌ์˜ ์ตœ์ ํ™” ํ”„๋ฆฌ์…‹ - + // ์นด๋ฉ”๋ผ ๋””๋ฐ”์ด์Šค guard // ํ›„๋ฉด ์นด๋ฉ”๋ผ ๊ฐ€์ ธ์˜ค๊ธฐ - let device = AVCaptureDevice.default(.builtInWideAngleCamera, + let device = AVCaptureDevice.default(.builtInWideAngleCamera, //ํ›„๋ฉด ์นด๋ฉ”๋ผ ์‚ฌ์šฉ ๋””๋ฒ„๊ทธ for: .video, position: .back), let input = try? AVCaptureDeviceInput(device: device), @@ -55,178 +49,375 @@ final class CameraManager: NSObject, ObservableObject { session.canAddInput(input) else { print("โŒ Camera input error") + session.commitConfiguration() //์„ค์ • ์™„๋ฃŒ return } - session.addInput(input)// ์นด๋ฉ”๋ผ ์ž…๋ ฅ ๋“ฑ๋ก + session.addInput(input) - // ์‚ฌ์ง„ ์ดฌ์˜ output ๋“ฑ๋ก - guard session.canAddOutput(photoOutput) else { + guard session.canAddOutput(photoOutput) else { //์‚ฌ์ง„ ์ฐ๊ธฐ ์ „์šฉ ์ถœ๋ ฅ print("โŒ Photo output error") + session.commitConfiguration() return } - session.addOutput(photoOutput) - - session.commitConfiguration()// ์„ค์ • ์™„๋ฃŒ - session.startRunning()// ์นด๋ฉ”๋ผ ์‹ค์ œ ์ž‘๋™ ์‹œ์ž‘ - } - - // ํ™”๋ฉด ์‚ฌ๋ผ์งˆ ๋•Œ ์นด๋ฉ”๋ผ ์ค‘์ง€ - func stopSession() { - session.stopRunning() - } + session.addOutput(photoOutput) //์นด๋ฉ”๋ผ&์‚ฌ์ง„ ์—ฐ๊ฒฐ ํŒŒ์ดํ”„ - // SwiftUI ROI ์ „๋‹ฌ - // ROIOverlay์—์„œ ๊ณ„์‚ฐ๋œ ์˜์—ญ์„ ์ €์žฅ - func updateROIRect(_ rect: CGRect) { - roiLayerRect = rect - } - - func sendToServer(imageData: Data) { - let locationDTO = capturedLocation?.toDTO() - - Task { - try await uploadService.upload( - imageData: imageData, - recognizedText: recognizedText, - location: locationDTO - ) - } + session.commitConfiguration() //์„ค์ • ์™„๋ฃŒ + isConfigured = true } - func debugPrintLocation() { - if let location = capturedLocation { - print("๐Ÿ“ latitude:", location.coordinate.latitude) - print("๐Ÿ“ longitude:", location.coordinate.longitude) - } else { - print("โŒ location is nil") + func startSession() { // ์นด๋ฉ”๋ผ ์ผœ์ง + sessionQueue.async { //์„ธ์…˜ ์ œ์–ด์šฉ ์ „์šฉ ํ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ + self.configureSession() + guard !self.session.isRunning else { return } + self.session.startRunning() } } - func debugPrintLocationDTO() { - guard let dto = capturedLocation?.toDTO() else { - print("โŒ LocationDTO is nil") - return + func stopSession() { + sessionQueue.async { + if self.session.isRunning { + self.session.stopRunning() + } } - - print("๐Ÿ“ฆ LocationDTO") - print(" - latitude:", dto.latitude) - print(" - longitude:", dto.longitude) } - // MARK: - ์‚ฌ์ง„ ์ดฌ์˜ - // ์‚ฌ์ง„ ์ดฌ์˜ ์‹œ์ž‘, ๊ฒฐ๊ณผ๋Š” delegate๋กœ ๋“ค์–ด์˜ด func capturePhoto() { - isProcessing = true - locationService.start() - let settings = AVCapturePhotoSettings() - photoOutput.capturePhoto(with: settings, delegate: self) + sessionQueue.async { //์ดฌ์˜ ์ „์šฉ ํ + guard self.session.isRunning else { return } + + let settings = AVCapturePhotoSettings() //๊ธฐ๋ณธ ์นด๋ฉ”๋ผ ์„ธํŒ… + + if let conn = self.photoOutput.connection(with: .video) { + if conn.isVideoOrientationSupported { + conn.videoOrientation = .portrait + } else if conn.isVideoRotationAngleSupported(90) { + conn.videoRotationAngle = 90 + } + } + + Task { @MainActor in //์ดฌ์˜ + self.isProcessing = true + } + + //์‹ค์ œ ์ดฌ์˜ ํ›„ delegate๋กœ ๊ฒฐ๊ณผ ๋ฐ›๊ธฐ + self.photoOutput.capturePhoto(with: settings, delegate: self) + } } -} - -// MARK: - AVCapturePhotoCaptureDelegate -extension CameraManager: AVCapturePhotoCaptureDelegate { + + @MainActor + func deleteCaptured(at index: Int) { // ์‚ฌ์ง„ ์‚ญ์ œ + guard capturedROIImages.indices.contains(index) else { return } - // Delegate๋Š” ๋ฉ”์ธ ์•กํ„ฐ ๋ฐ–์—์„œ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์–ด์„œ nonisolated๋กœ ๋‘  - nonisolated func photoOutput(_ output: AVCapturePhotoOutput, - didFinishProcessingPhoto photo: AVCapturePhoto, - error: Error?) { + capturedROIImages.remove(at: index) - if let error { - print("โŒ Capture error:", error) - return + if OCRFilters.indices.contains(index) { + OCRFilters.remove(at: index) } + } + + @MainActor + func resetBatch() { + capturedROIImages.removeAll() + OCRFilters.removeAll() + recognizedText = "" + } - guard - // ์ดฌ์˜๋œ ์‚ฌ์ง„ -> UIImage -> CGImage (Vision์€ CGImage ํ•„์š”) - let data = photo.fileDataRepresentation(), - let image = UIImage(data: data), - let cgImage = image.cgImage - else { return } - - // @MainActor ์†์„ฑ(previewLayer/roiLayerRect)์€ ๋ฉ”์ธ ์•กํ„ฐ์—์„œ๋งŒ ์ฝ์„ ์ˆ˜ ์žˆ์Œ - Task { @MainActor in - self.capturedLocation = self.locationService.currentLocation - self.debugPrintLocation() - self.debugPrintLocationDTO() - let layer = self.previewLayer - let roi = self.roiLayerRect - - guard let layer else { - self.isProcessing = false + + +} //final class + + + // MARK: - AVCapturePhotoCaptureDelegate + extension CameraManager: AVCapturePhotoCaptureDelegate { //์ดฌ์˜ ๊ฒฐ๊ณผ delegate๋กœ ๋ฐ›์•„์˜ค๊ธฐ + + /// Delegate๋Š” ๋ฉ”์ธ ์•กํ„ฐ ๋ฐ–์—์„œ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์–ด์„œ nonisolated๋กœ ๋‘  + nonisolated func photoOutput(_ output: AVCapturePhotoOutput, + didFinishProcessingPhoto photo: AVCapturePhoto, + error: Error?) { + + if let error { + print("โŒ Capture error:", error) return } - - // ๋ฌด๊ฑฐ์šด OCR์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ - Task.detached { [layer, roi] in - // ์ˆœ์ˆ˜ OCR ํ•จ์ˆ˜ ํ˜ธ์ถœ - let text = CameraManager.performOCR( - cgImage: cgImage, - previewLayer: layer, - roiLayerRect: roi - ) - - // UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์•กํ„ฐ์—์„œ - await MainActor.run { - self.recognizedText = text + + guard let data = photo.fileDataRepresentation() else { return } //์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ๋กœ ๋ณ€ํ™˜ + + // MainActor์—์„œ UIKit ์ž‘์—… ์ฒ˜๋ฆฌ + Task { @MainActor in + guard let rawImage = UIImage(data: data) else { //์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + self.isProcessing = false + return + } + + // ์—ฌ๊ธฐ์„œ ์ •๊ทœํ™” + let image = rawImage.normalizedUp() // ์ด๋ฏธ์ง€ ๋‹ค์‹œ ๊ทธ๋ ค์„œ .up ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ + + guard let cgImage = image.cgImage else { //ํ”ฝ์…€ ๋ฐฐ์—ด ์ด๋ฏธ์ง€ (width x height) + self.isProcessing = false + return + } + + let layer = self.previewLayer + guard let layer else { self.isProcessing = false - self.capturedLocation = self.locationService.currentLocation + return + } + + //UI ๋ฐ•์Šค์™€ ๋™์ผํ•œ ๊ณต์‹์œผ๋กœ ROI ๊ณ„์‚ฐ + let roi = roiRect(in: layer.bounds.size) + + Task.detached { [layer, roi] in + let cropped = CameraManager.cropToROI( //ROI ๊ธฐ์ค€์œผ๋กœ ์ด๋ฏธ์ง€ ํฌ๋กญ + cgImage: cgImage, + previewLayer: layer, + roiLayerRect: roi + ) + + let text = CameraManager.performOCR( //OCR ์ˆ˜ํ–‰ + cgImage: cgImage, + previewLayer: layer, + roiLayerRect: roi + ) + + await MainActor.run { + //ROI ๋ฐ•์Šค๋ž‘ ๊ฐ™์€ ์‚ฌ์ด์ฆˆ๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ + let scale = layer.contentsScale // ๋ณดํ†ต 2.0 / 3.0 + let targetSize = CGSize( + width: roi.width * scale, + height: roi.height * scale + ) + + self.recognizedText = text // ๋งˆ์ง€๋ง‰ OCR (UI๋ณด์—ฌ์ฃผ๋Š”์šฉ) + + // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์•„๋ฌด๊ฒƒ๋„ ์ €์žฅ ์•ˆํ•จ + guard let item = Self.parseItem(from: text) else { + self.isProcessing = false + return + } + + // ํŒŒ์‹ฑ ์„ฑ๊ณต ์‹œ + if let cropped { + let resized = cropped.resized(to: targetSize) //๋ฆฌ์‚ฌ์ด์ฆˆ ์ ์šฉ +// self.croppedROIImage = resized // ๋งˆ์ง€๋ง‰ 1์žฅ + self.capturedROIImages.append(resized) // ์ด๋ฏธ์ง€ ๋ˆ„์  + self.OCRFilters.append(item) // ํŒŒ์‹ฑํ•œ OCR ๊ฒฐ๊ณผ ๋ˆ„์  + } + +// if let item = Self.parseItem(from: text) { +// self.OCRFilters.append(item) // ํŒŒ์‹ฑํ•œ OCR ๊ฒฐ๊ณผ ๋ˆ„์  +// } +// +// if !text.isEmpty { +// self.capturedTexts.append(text) // OCR ๋ˆ„์  +// } + + self.isProcessing = false + } } } - } // task + } } -} - -// MARK: - OCR (Vision) -extension CameraManager { - // MainActor์™€ ๋ถ„๋ฆฌ๋œ "์ˆœ์ˆ˜ OCR ํ•จ์ˆ˜" - // - background(Task.detached)์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ํ˜ธ์ถœ ๊ฐ€๋Šฅ - // nonisolated: ์–ด๋А ์Šค๋ ˆ๋“œ์—์„œ๋„ ํ˜ธ์ถœ ๊ฐ€๋Šฅ - // static: ์ƒํƒœ ์—†๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜ - nonisolated static func performOCR( - cgImage: CGImage, - previewLayer: AVCaptureVideoPreviewLayer, - roiLayerRect: CGRect - ) -> String { - - // 1) SwiftUI ROI(layer ์ขŒํ‘œ) -> ์นด๋ฉ”๋ผ ์ •๊ทœํ™” ์ขŒํ‘œ(0~1, origin=top-left) - let metadataROI = - previewLayer.metadataOutputRectConverted(fromLayerRect: roiLayerRect)// SwiftUI ์ขŒํ‘œ -> ์นด๋ฉ”๋ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ขŒํ‘œ(0~1) + + // MARK: - OCR (Vision) + extension CameraManager { //MainActor์™€ ๋ถ„๋ฆฌ๋œ OCR ํ•จ์ˆ˜ (Task.detached์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ํ˜ธ์ถœ ๊ฐ€๋Šฅ) + nonisolated + static func videoRectInLayer(bounds: CGRect, imageSize: CGSize) -> CGRect { // ์‹ค์ œ ์˜์ƒ์ด๋ž‘ ๋ ˆ์ด์–ด ์˜์—ญ ๊ณ„์‚ฐ + let layerW = bounds.width + let layerH = bounds.height + + let imageAspect = imageSize.width / imageSize.height + let layerAspect = layerW / layerH + + if imageAspect > layerAspect { + // ์˜์ƒ์ด ๋” ๋„“์Œ โ†’ ๋†’์ด์— ๋งž์ถฐ ์ฑ„์šฐ๋ฉด ์ขŒ์šฐ๊ฐ€ ์ž˜๋ฆผ + let videoH = layerH + let videoW = videoH * imageAspect + let x = (layerW - videoW) / 2 + return CGRect(x: x, y: 0, width: videoW, height: videoH) + } else { + // ์˜์ƒ์ด ๋” ๊ธธ์ญ‰ํ•จ โ†’ ๋„ˆ๋น„์— ๋งž์ถฐ ์ฑ„์šฐ๋ฉด ์ƒํ•˜๊ฐ€ ์ž˜๋ฆผ + let videoW = layerW + let videoH = videoW / imageAspect + let y = (layerH - videoH) / 2 + return CGRect(x: 0, y: y, width: videoW, height: videoH) + } + } - // 2) Vision ROI๋Š” origin์ด bottom-left๋ผ์„œ y๋ฅผ ๋’ค์ง‘์–ด์•ผ ํ•จ - let visionROI = CGRect( - x: metadataROI.origin.x,// Vision ์ขŒํ‘œ๊ณ„๋Š” ์ขŒํ•˜๋‹จ origin - y: 1 - metadataROI.origin.y - metadataROI.height,// y์ถ• ๋’ค์ง‘๊ธฐ - width: metadataROI.width, - height: metadataROI.height - ) - - let request = VNRecognizeTextRequest()// OCR ์š”์ฒญ ๊ฐ์ฒด - request.recognitionLevel = .accurate - request.regionOfInterest = visionROI// ROI ๋‚ด๋ถ€๋งŒ OCR - request.usesLanguageCorrection = true - request.recognitionLanguages = ["ko-KR", "en-US"] - request.automaticallyDetectsLanguage = false - + nonisolated + static func normalizedRectFromLayerRect( // ์นด๋ฉ”๋ผ ์ด๋ฏธ์ง€ ๊ธฐ์ค€ ์ •๊ทœํ™” ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ + _ layerRect: CGRect, + layerBounds: CGRect, + imageSize: CGSize + ) -> CGRect { + let videoRect = videoRectInLayer(bounds: layerBounds, imageSize: imageSize) + + let nx = (layerRect.minX - videoRect.minX) / videoRect.width + let ny = (layerRect.minY - videoRect.minY) / videoRect.height + let nw = layerRect.width / videoRect.width + let nh = layerRect.height / videoRect.height + + func clamp(_ v: CGFloat) -> CGFloat { min(max(v, 0), 1) } + + let x = clamp(nx) + let y = clamp(ny) + + // nx/ny๊ฐ€ ์Œ์ˆ˜๋ฉด width/height๋„ ๊ฐ™์ด ์ค„์—ฌ์„œ clamp๋˜๊ฒŒ ๋ณด์ • + let w = clamp(nw + min(0, nx)) + let h = clamp(nh + min(0, ny)) + + return CGRect(x: x, y: y, width: w, height: h) + } - // โš ๏ธ ๊ธฐ๊ธฐ ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Œ (์ผ๋‹จ portrait ๊ธฐ์ค€์œผ๋กœ .right) - let handler = VNImageRequestHandler( - cgImage: cgImage, - orientation: .up, - options: [:] - ) + nonisolated + static func performOCR( // ROI ์˜์—ญ๋งŒ ํ…์ŠคํŠธ ์ธ์‹ํ•ด์„œ String ๋ฐ˜ํ™˜ + cgImage: CGImage, //์›๋ณธ ํ”ฝ์…€ ์ด๋ฏธ์ง€ + previewLayer: AVCaptureVideoPreviewLayer, //ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด + roiLayerRect: CGRect //ROI ์ขŒํ‘œ๊ณ„ + ) -> String { + + let imageSize = CGSize(width: cgImage.width, height: cgImage.height) + + // ์ง์ ‘ ๊ณ„์‚ฐํ•œ ์นด๋ฉ”๋ผ ์ •๊ทœํ™” ์ขŒํ‘œ ROI (top-left) + let normROI = normalizedRectFromLayerRect( + roiLayerRect, + layerBounds: previewLayer.bounds, + imageSize: imageSize + ) + + // Vision์€ origin์ด bottom-left๋ผ y ๋’ค์ง‘๊ธฐ + let visionROI = CGRect( + x: normROI.origin.x, + y: 1 - normROI.origin.y - normROI.height, + width: normROI.width, + height: normROI.height + ) + + let request = VNRecognizeTextRequest() //ํ…์ŠคํŠธ ์ธ์‹ ์š”์ฒญ ๊ฐ์ฒด + request.recognitionLevel = .accurate //์ธ์‹ ์ •ํ™•๋„ + request.regionOfInterest = visionROI //ROI ์ƒ์ž๋งŒ + request.usesLanguageCorrection = true //ํ›„๋ณด์ • + request.recognitionLanguages = ["ko-KR", "en-US"] //์–ธ์–ด ์„ค์ • + request.automaticallyDetectsLanguage = false //์ž๋™์–ธ์–ด๊ฐ์ง€ ์˜คํ”„ + + + // Vision ์š”์ฒญ ์‹คํ–‰๊ธฐ + let handler = VNImageRequestHandler( + cgImage: cgImage, + orientation: .up, + options: [:] + ) + + //OCR ์‹คํ–‰ + do { + try handler.perform([request]) + } catch { + return "โŒ Vision error: \(error.localizedDescription)" + } + + //๊ฒฐ๊ณผ ๊บผ๋‚ด๊ธฐ + let results = request.results as? [VNRecognizedTextObservation] ?? [] + + //๊ฒฐ๊ณผ ๋ฌธ์ž์—ด๋กœ ํ•ฉ์น˜๊ธฐ + return results + .compactMap { $0.topCandidates(1).first?.string } + .joined(separator: "\n") + + } - do { - try handler.perform([request]) - } catch { - return "โŒ Vision error: \(error.localizedDescription)" + nonisolated + static func cropToROI( //ROI ์˜์—ญ๋งŒ ์ž˜๋ผ์„œ UIImage ๋ฐ˜ํ™˜ + cgImage: CGImage, + previewLayer: AVCaptureVideoPreviewLayer, + roiLayerRect: CGRect + ) -> UIImage? { + + //์›๋ณธ ์ด๋ฏธ์ง€ ํ”ฝ์…€ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ + let W = CGFloat(cgImage.width) + let H = CGFloat(cgImage.height) + let imageSize = CGSize(width: W, height: H) + + // ์ง์ ‘ ๊ณ„์‚ฐํ•œ ์ •๊ทœํ™” ROI (top-left) + let normROI = normalizedRectFromLayerRect( + roiLayerRect, + layerBounds: previewLayer.bounds, + imageSize: imageSize + ) + + // ํ”ฝ์…€ rect๋กœ ๋ณ€ํ™˜ (์ด๊ฑด top-left ๊ธฐ์ค€ ์‹คํ–‰) + var rect = CGRect( + x: normROI.origin.x * W, + y: normROI.origin.y * H, + width: normROI.width * W, + height: normROI.height * H + ).integral + +// print("normROI:", normROI) +// print("pixelRect:", rect) +// print("layer.bounds:", previewLayer.bounds) +// print("cgImage:", cgImage.width, "x", cgImage.height) + + //์ด๋ฏธ์ง€ ๊ฒฝ๊ณ„ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ„๊ฑฐ ์ž˜๋ผ๋‚ด๊ธฐ + rect = rect.intersection(CGRect(x: 0, y: 0, width: W, height: H)) + guard !rect.isNull, rect.width > 1, rect.height > 1 else { return nil } + + //ํฌ๋กญ ์ˆ˜ํ–‰ + guard let croppedCG = cgImage.cropping(to: rect) else { return nil } + //UIImage๋กœ ๋ฐ˜ํ™˜ + return UIImage(cgImage: croppedCG) + } - let results = request.results as? [VNRecognizedTextObservation] ?? [] + static func parseItem(from text: String) -> OCRFilter? { //OCR ํŒŒ์‹ฑ ํ•จ์ˆ˜ (๊ฐ€๊ฒฉ + ์ƒํ’ˆ๋ช… ์ถ”์ถœ) + //๊ณต๋ฐฑ ์ œ๊ฑฐ + let lines = text + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } - // OCR ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด ํ•ฉ์น˜๊ธฐ - return results - .compactMap { $0.topCandidates(1).first?.string } - .joined(separator: "\n") + // ๊ฐ€๊ฒฉ ์ถ”์ถœ: ์ค„์—์„œ ์ˆซ์ž๋งŒ ๋ฝ‘์•„ Int๋กœ ๋ณ€ํ™˜ + var bestPrice: Int? = nil + for line in lines.reversed() { //์•„๋žซ์ค„๋ถ€ํ„ฐ ๊ฒ€์‚ฌ + let digits = line.filter { $0.isNumber } // ์ˆซ์ž๋งŒ ๋ฝ‘๊ธฐ (์ฝค๋งˆ/์›/โ‚ฉ ์ž๋™ ์ œ๊ฑฐ) + + if digits.count >= 4, digits.count <= 6, let v = Int(digits) { // 4์ž๋ฆฌ~6์ž๋ฆฌ๋งŒ ๊ฐ€๊ฒฉ์œผ๋กœ ๊ฐ€์ • + bestPrice = v + break + } + } + + guard let price = bestPrice else { return nil } //๊ฐ€๊ฒฉ ์ฐพ๊ธฐ ์‹คํŒจํ•˜๋ฉด ๋ฆฌํ„ด + + // ์ƒํ’ˆ๋ช… ์ถ”์ถœ: ์ˆซ์ž๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ค„์€ ์ œ์™ธํ•˜๊ณ  ๊ฐ€์žฅ ๊ธด ์ค„์„ ์ด๋ฆ„์œผ๋กœ + let nameCandidates = lines.filter { line in + let digitCount = line.filter { $0.isNumber }.count // ์ค„ ์•ˆ์— ์ˆซ์ž ๊ฐœ์ˆ˜ ์นด์šดํŠธ + return digitCount <= max(2, line.count / 5) //์ˆซ์ž ์ค„ ๊ธธ์ด์˜ 20%๋งŒ, ์ตœ๋Œ€ 2๊ฐœ๊นŒ์ง€๋งŒ ํ—ˆ์šฉ + } + + //ํ›„๋ณด ์ค‘ ๊ฐ€์žฅ ๊ธด ์ค„์„ ์ฑ„ํƒ + let name = (nameCandidates.max(by: { $0.count < $1.count }) ?? lines.first ?? "").trimmingCharacters(in: .whitespaces) + + guard !name.isEmpty else { return nil } + return OCRFilter(name: name, price: price, rawText: text) + } + } + + extension UIImage { + // imageOrientation(๋ฉ”ํƒ€)๋ฅผ ํ”ฝ์…€์— ๋ฐ˜์˜ํ•ด์„œ ์‹ค์ œ๋กœ .up์ธ ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“ค์–ด์คŒ + func normalizedUp() -> UIImage { + if imageOrientation == .up { return self } //๋ฐฉํ–ฅ .up์œผ๋กœ + + let format = UIGraphicsImageRendererFormat.default() + format.scale = 1 // ํ•ด์ƒ๋„๋ฅผ ์›๋ณธ ํ”ฝ์…€ ๊ธฐ์ค€์œผ๋กœ ์ฒ˜๋ฆฌ(1pt = 1px) + + return UIGraphicsImageRenderer(size: size, format: format).image { _ in + draw(in: CGRect(origin: .zero, size: size)) //ํ”ฝ์…€ ์ž์ฒด๋กœ ํšŒ์ „๋œ ์ƒํƒœ๋กœ ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ + } + } + func resized(to targetSize: CGSize) -> UIImage { //์‚ฌ์ง„ ๋ฆฌ์‚ฌ์ด์ฆˆ + let renderer = UIGraphicsImageRenderer(size: targetSize) + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: targetSize)) + } + } } -} diff --git a/Shoppingmate_Frontend/ViewModel/LocationSelectViewModel.swift b/Shoppingmate_Frontend/ViewModel/LocationSelectViewModel.swift new file mode 100644 index 0000000..da8f87c --- /dev/null +++ b/Shoppingmate_Frontend/ViewModel/LocationSelectViewModel.swift @@ -0,0 +1,122 @@ +// +// LocationSelectViewModel.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/5/26. +// + +import Foundation +import MapKit +import CoreLocation +import Combine +import SwiftUI + +final class LocationSelectViewModel: ObservableObject { + + // ์ง€๋„ ์ƒํƒœ + @Published var region: MKCoordinateRegion + + @Published var address: String? = nil// ํ˜„์žฌ ์ง€๋„ ์ค‘์‹ฌ ์ขŒํ‘œ์˜ ์ฃผ์†Œ + + @Published var isConfirmed: Bool = false// "์ด ์œ„์น˜๊ฐ€ ๋งž์•„์š”" ๋ˆŒ๋ €๋Š”์ง€ ์—ฌ๋ถ€ + + @Published private var shouldMoveToCurrentLocation = false + + var selectedLocation: LocationInfo?// ์ตœ์ข… ํ™•์ •๋œ ์œ„์น˜ (๋‹ค์Œ ํ™”๋ฉด์œผ๋กœ ์ „๋‹ฌ) + + private let geocoder = CLGeocoder()// ์ขŒํ‘œ ์ฃผ์†Œ ๋ณ€ํ™˜ + + private let locationService: LocationService + private var cancellables = Set() + @EnvironmentObject var serverViewModel: ServerViewModel + + // ์ดˆ๊ธฐํ™” + init(locationService: LocationService) { + self.locationService = locationService + + // ์ตœ์ดˆ ์ง€๋„ ์œ„์น˜ (์•ž์—์„œ ๋ฐ›์•„์˜จ ํ˜„์žฌ ์‚ฌ์šฉ์ž ์œ„์น˜๋กœ ๋ฐ›์•„์˜ค๋Š”๊ฑธ๋กœ ์ˆ˜์ •ํ•ด์•ผ๋จ) + self.region = MKCoordinateRegion( + center: CLLocationCoordinate2D( + latitude: 37.5665, + longitude: 126.9780 + ), + span: MKCoordinateSpan(//์–ผ๋งˆ๋‚˜ ๋„“๊ฒŒ ๋ณด์—ฌ์ค„๊ฑด์ง€(์คŒ ๋ ˆ๋ฒจ) + latitudeDelta: 0.0005, + longitudeDelta: 0.0005 + ) + ) + bindLocation() + } + + private func bindLocation() { + locationService.$currentLocation + .compactMap { $0 }// nil ์ œ๊ฑฐ + .sink { [weak self] location in + guard let self else { return } + // ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ๋งŒ ์ง€๋„ ์ด๋™ + if self.shouldMoveToCurrentLocation { + print("๐Ÿ—บ๏ธ ํ˜„์žฌ ์œ„์น˜๋กœ ์ง€๋„ ์ด๋™") + withAnimation { + self.region.center = location.coordinate + } + // ์ฃผ์†Œ๋„ ํ˜„์žฌ ์œ„์น˜ ๊ธฐ์ค€์œผ๋กœ ๊ฐฑ์‹  + self.reverseGeocode(location.coordinate) + // 1ํšŒ ์ฒ˜๋ฆฌ ํ›„ ๋ฆฌ์…‹ + self.shouldMoveToCurrentLocation = false + } + } + .store(in: &cancellables) + } + + // ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ + func moveToCurrentLocation() { + print("๐Ÿ“Œ ํ˜„์žฌ ์œ„์น˜ ๋ฒ„ํŠผ ํด๋ฆญ") + + // ๋‹ค์Œ ์œ„์น˜ ์ˆ˜์‹  ์‹œ ์ง€๋„ ์ด๋™ํ•˜๋ผ๊ณ  ํ‘œ์‹œ + shouldMoveToCurrentLocation = true + + locationService.requestCurrentLocation() + } + + // ์ง€๋„ ์ด๋™ ๊ฐ์ง€ + // ์ง€๋„๋ฅผ ๋“œ๋ž˜๊ทธํ•ด์„œ ์ค‘์‹ฌ ์ขŒํ‘œ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ ํ˜ธ์ถœ + func onMapMoved() { + reverseGeocode(region.center) + } + + // ์œ„์น˜ ํ™•์ •: '์ด ์œ„์น˜๋กœ ์„ค์ •' ๋ฒ„ํŠผ + func confirmLocation() { +// guard let address else { +// print("โŒ ์ฃผ์†Œ๊ฐ€ ์•„์ง ์—†์Šต๋‹ˆ๋‹ค") +// return +// } +// +// // ํ˜„์žฌ ์ง€๋„ ์ค‘์‹ฌ + ์ฃผ์†Œ๋ฅผ ๋ฌถ์Œ +// selectedLocation = LocationInfo( +// coordinate: region.center, +// address: address +// ) + + // NavigationStack ํŠธ๋ฆฌ๊ฑฐ/ + isConfirmed = true + } + + // ์ขŒํ‘œ ์ฃผ์†Œ ๋ณ€ํ™˜ + private func reverseGeocode(_ coordinate: CLLocationCoordinate2D) { + + let location = CLLocation( + latitude: coordinate.latitude, + longitude: coordinate.longitude + ) + + geocoder.reverseGeocodeLocation(location) { placemarks, _ in + guard let place = placemarks?.first else { return } + + // ์ฃผ์†Œ ๊ตฌ์„ฑ (ํ•„์š”์— ๋”ฐ๋ผ ์ˆ˜์ • ๊ฐ€๋Šฅ) + self.address = + [place.name, place.locality] + .compactMap { $0 } + .joined(separator: " ") + } + } +} diff --git a/Shoppingmate_Frontend/ViewModel/LoginViewModel.swift b/Shoppingmate_Frontend/ViewModel/LoginViewModel.swift new file mode 100644 index 0000000..e793084 --- /dev/null +++ b/Shoppingmate_Frontend/ViewModel/LoginViewModel.swift @@ -0,0 +1,86 @@ +// +// LoginViewModel.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 12/31/25. +// + +import Foundation +import CoreLocation +import Combine + +final class LoginViewModel: ObservableObject { + @Published var isUserReady: Bool = false + + //@EnvironmentObject var serverViewModel: ServerViewModel + + let locationService = LocationService() + let uploadService = UploadService() + + init() {} + + enum UserDefaultKey { + static let isNormalUser = "isNormalUser" + static let uuid = "guest_uuid" + } + + //UUID ์ƒ์„ฑ ํ•จ์ˆ˜ + private func getOrCreateUUID() -> String { + if let uuid = UserDefaults.standard.string(forKey: UserDefaultKey.uuid) { + return uuid + } + + let newUUID = UUID().uuidString + UserDefaults.standard.set(newUUID, forKey: UserDefaultKey.uuid) + return newUUID + } + + /// ๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ ๋ˆ„๋ฅผ ์‹œ ํ˜ธ์ถœ + func guestLogin() { + print("๐ŸŸข ๊ฒŒ์ŠคํŠธ ๋กœ๊ทธ์ธ") + //์œ„์น˜ ์š”์ฒญ + locationService.requestOneTimeLocation() + + // UUID ์ƒ์„ฑ + let uuid = getOrCreateUUID() + print("๐Ÿ†” UUID:", uuid) + + // UUID DTO ์ƒ์„ฑ + let uuidDTO = UUIDDTO(uuid: uuid) + + //Task์—์„œ ์„œ๋ฒ„ํ†ต์‹  + Task { + do { + // ์œ ์ € ํƒ€์ž… ์ €์žฅ (์ฒซ ํŽ˜์ด์ง€ ์žฌ๋…ธ์ถœ ๋ฐฉ์ง€) + let response = try await uploadService.uploadUUID(uuid: uuidDTO) + + UserDefaults.standard.set(response.userId, forKey: "userId") + //UUID ๋กœ๊ทธ์ธ POST + UserDefaults.standard.set(true, forKey: UserDefaultKey.isNormalUser) + + DispatchQueue.main.async { + self.isUserReady = true + } + //try await uploadService.uploadUUID(uuid: uuidDTO) + print("โœ… UUID ๋กœ๊ทธ์ธ ์„ฑ๊ณต") + + } catch { + print("๐Ÿšจ guestLogin ์‹คํŒจ:", error) + } + } + + // ์„œ๋ฒ„ ๋กœ๊ทธ์ธ (์ถ”ํ›„ ์—ฐ๊ฒฐ) + //loginGuest(uuid: uuid) + } + + + /// ๋””๋ฒ„๊ทธ์šฉ (์„ ํƒ) + func debugPrintLocation() { + if let location = locationService.currentLocation { + print("๐Ÿ“ latitude:", location.coordinate.latitude) + print("๐Ÿ“ longitude:", location.coordinate.longitude) + } else { + print("โŒ location is nil") + } + } +} diff --git a/Shoppingmate_Frontend/ViewModel/ServerViewModel.swift b/Shoppingmate_Frontend/ViewModel/ServerViewModel.swift new file mode 100644 index 0000000..79730e3 --- /dev/null +++ b/Shoppingmate_Frontend/ViewModel/ServerViewModel.swift @@ -0,0 +1,135 @@ +// +// ServerViewModel.swift +// Shoppingmate_Frontend +// +// Created by ์†์ฑ„์› on 1/6/26. +// + +import SwiftUI +import CoreLocation +import Combine + +final class ServerViewModel: NSObject, ObservableObject { + + @Published var isUserReady: Bool = false + private let loginViewModel: LoginViewModel + private let uploadService = UploadService() + private let locationService = LocationService() + private var capturedLocation: CLLocation? + + init(loginViewModel: LoginViewModel) { + self.loginViewModel = loginViewModel + } + + + func debugPrintLocation() { + if let location = capturedLocation { + print("๐Ÿ“ latitude:", location.coordinate.latitude) + print("๐Ÿ“ longitude:", location.coordinate.longitude) + } else { + print("โŒ location is nil") + } + } + + func debugPrintLocationDTO(_ dto: LocationDTO) { + let userId = UserDefaults.standard.integer(forKey: "userId") + guard let dto = capturedLocation?.toDTO(userId: userId) else { + print("โŒ LocationDTO is nil") + return + } + + print("๐Ÿ“ฆ LocationDTO") + print(" - latitude:", dto.latitude) + print(" - longitude:", dto.longitude) + } + + + func handleLocationAfterLogin() { + Task { + // userId ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ (์ตœ๋Œ€ 2์ดˆ) + var userId: Int = 0 + for _ in 0..<20 { + userId = UserDefaults.standard.integer(forKey: "userId") + if userId != 0 { + break + } + try await Task.sleep(nanoseconds: 100_000_000) // 0.1์ดˆ + } + + guard userId != 0 else { + print("โŒ userId ๋๊นŒ์ง€ ์ค€๋น„ ์•ˆ ๋จ") + return + } + + print("โœ… userId ์ค€๋น„ ์™„๋ฃŒ:", userId) + + // ์œ„์น˜๊ฐ€ ์•„์ง ์—†์œผ๋ฉด ์ž ๊น ๋Œ€๊ธฐ (์ตœ๋Œ€ 1์ดˆ) + for _ in 0..<10 { + if locationService.currentLocation != nil { + break + } + try await Task.sleep(nanoseconds: 100_000_000) // 0.1์ดˆ + } + + // ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ + guard let location = locationService.currentLocation else { + print("โŒ ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•จ") + return + } + + self.capturedLocation = location + + // ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (๊ธฐ์กด ๊ทธ๋Œ€๋กœ) + self.debugPrintLocation() + + let locationDTO = location.toDTO(userId: userId) + self.debugPrintLocationDTO(locationDTO) + + // ์„œ๋ฒ„ ์ „์†ก (PATCH) + do { + try await uploadService.updateLocation(location: locationDTO) + print("โœ… ๋กœ๊ทธ์ธ ํ›„ ์œ„์น˜ ์„œ๋ฒ„ ์ „์†ก ์„ฑ๊ณต") + } catch { + print("โŒ ๋กœ๊ทธ์ธ ํ›„ ์œ„์น˜ ์„œ๋ฒ„ ์ „์†ก ์‹คํŒจ:", error) + } + } + } + + func handleLocationUpdateAfterButton() { + Task { + // userId ์ค€๋น„๋๋Š”์ง€ ๋จผ์ € ํ™•์ธ + guard isUserReady else { + print("โณ userId ์•„์ง ์ค€๋น„ ์•ˆ ๋จ, ์œ„์น˜ ์—…๋ฐ์ดํŠธ ๋ณด๋ฅ˜") + return + } + // ์œ„์น˜ ๋“ค์–ด์˜ฌ ๋•Œ๊นŒ์ง€ ์ž ๊น ๋Œ€๊ธฐ + for _ in 0..<10 { + if locationService.currentLocation != nil { + break + } + try await Task.sleep(nanoseconds: 100_000_000) + } + + guard let location = locationService.currentLocation else { + print("โŒ ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•จ (UPDATE)") + return + } + + let userId = UserDefaults.standard.integer(forKey: "userId") + guard userId != 0 else { + print("โŒ userId ์—†์Œ") + return + } + + let dto = location.toDTO(userId: userId) + + do { + try await uploadService.updateLocation(location: dto) + print("๐Ÿ”„ ์œ„์น˜ UPDATE ์„ฑ๊ณต") + } catch { + print("โŒ ์œ„์น˜ UPDATE ์‹คํŒจ:", error) + } + } + } +} + diff --git a/Shoppingmate_Frontend/logo.mp4 b/Shoppingmate_Frontend/logo.mp4 new file mode 100644 index 0000000..0b91576 Binary files /dev/null and b/Shoppingmate_Frontend/logo.mp4 differ