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) ์จ๋ณด๋ฉ
+์ฑ์ ๋ค์ด๊ฐ๋ ์ฒซ ํ๋ฉด์
๋๋ค.
+
+
+
+
+---
+
+### 2) ๊ฒ์คํธ ๋ก๊ทธ์ธ
+UUID๋ฅผ ํตํด ๊ฒ์คํธ ๋ก๊ทธ์ธ์ ํ ์ ์์ต๋๋ค.
+
+
+
+
+---
+
+### 3) ์์น ์ค์
+ํ์ฌ ์์น๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฃผ๋ณ ๋งํธ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
+
+
+
+
+---
+
+### 4) OCR ๊ธฐ๋ฅ ์คํ
+๊ฐ๊ฒฉํ๋ฅผ ์ดฌ์ํ์ฌ ์ํ ์ ๋ณด๋ฅผ ์๋์ผ๋ก ์ธ์ํฉ๋๋ค.
+
+1) OCR ํ๋ฉด ์คํ ์งํ
+
+
+2) OCR ์ค์บ ํ๋ฉด
+
+
+---
+
+### 5) OCR ๊ฒฐ๊ณผ ํ๋ฉด
+์จยท์คํ๋ผ์ธ ๊ฐ๊ฒฉ์ ๋น๊ตํ๊ณ , 1ํ ์ฌ์ฉ ๋น ๊ฐ๊ฒฉ ์ ๋ณด๋ฅผ ์ ๊ณตํฉ๋๋ค.
+
+
+
+
+---
+
+### 6) ๊ฐ์ฑ๋น ์ธ๋ถ์ ๋ณด ํ๋ฉด
+AI๋ฅผ ํตํด ๊ฐ์ฑ๋น ์ ์๋ฅผ ์ฐ์ถํ๊ณ , ๊ตฌ๋งค ์ถ์ฒ ๋ฐ ํ์ง๊ณผ ๊ฐ๊ฒฉ ์์ฝ, ํด๋น ์ ํ์ ์์ธ ์ ๋ณด๋ฅผ ์ ๊ณตํฉ๋๋ค.
+
+
+
+
+
+
+
+---
+
+## ๐ฉโ๐ป ๊ฐ๋ฐ์
+
+| ์ด๋ฆ | ์ญํ |
+|----|----|
+| ์์ฑ์ | 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