-
๋ง์๋ญ์ ๋ง์ง์ ์ฌ๋ํ๋ ์ฌ์ฉ์๋ค์ ์ํด ์ค๊ณ๋ ์ฑ์ผ๋ก, ๋ฐฉ๋ฌธํ ๋ง์ง์ ๊ธฐ๋กํ๊ณ , ๋๋ง์ ๋ฏธ์ ์ง๋๋ฅผ ๋ง๋ค์ด ํน๋ณํ ์ถ์ต์ ๊ฐ์งํ ์ ์๋๋ก ๋์ต๋๋ค.
-
์ฌ๋ฌ๋ถ์ ์ง์ ๋ง์ง์ "๋ง์๋ญ ์คํ"๋ฅผ ๋ถ์ฌํ๋ฉฐ, ๋๋ง์ ๋ฏธ์ ๊ธฐ์ค์ ๋ง๋ค์ด๊ฐ ์ ์์ต๋๋ค. ์ด ์ฑ์ ๋จ์ํ ์ ๋ณด๋ฅผ ๊ธฐ๋กํ๋ ๊ฒ์ ๋์ด, ์ฌ๋ฌ๋ถ์ ๋ฏธ์ ์ฌ์ ์ ์๊ฐ์ ์ผ๋ก ํํํ๊ณ , ์ถ์ต์ ๊ณต์ ํ๋ฉฐ, ์๋ก์ด ๊ฒฝํ์ ๋ฐ๊ฒฌํ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค.
-
์ฌ์ง๊ณผ ๋ฆฌ๋ทฐ๋ฅผ ํตํด ๋ง์ง์ ํ๋์ ๊ด๋ฆฌํ๊ณ , ์ง๋ ๊ธฐ๋ฐ์ผ๋ก ๋๋ง์ ๋ฏธ์ ์ง๋๋ฅผ ๋ง๋ค์ด๋ณด์ธ์.
-
๋ง์๋ญ๊ณผ ํจ๊ป๋ผ๋ฉด, ํ๋ฒํ ํ๋ฃจ๋ ํน๋ณํ ๋ฏธ์ ๊ฒฝํ์ผ๋ก ๋ฐ๋๊ฒ ๋ฉ๋๋ค. ๋ง์๋ ์๊ฐ์ ๊ธฐ๋กํ๊ณ , ์๋ก์ด ๋ฏธ์ ์ธ๊ณ๋ฅผ ํํํด๋ณด์ธ์!
- ์ฑ์คํ ์ด ์์ ์นดํ ๊ณ ๋ฆฌ ์ฐจํธ ์ต๊ณ ์์ 30์
- MAU ํ๊ท 200๋ช , ๋ค์ด๋ก๋ ์ 1200ํ
- ํ๊ท ๋ณ์ (4.8/5.0)์
- 2024.10.01 ~ 2024.11.1 (4์ฃผ) + ํ์ฌ ์งํ์ค
- ๊ฐ์ธ(1์ธ) ํ๋ก์ ํธ
ํ์ฌ ๋ง์๋ญ์ 1.0์ ์ฑ๊ณต์ ๊ธฐ๋ฐ์ผ๋ก, ์๋ฒ์ AI ๊ธฐ์ ์ ๋์
ํ์ฌ ๋์ฑ ๋ฐ์ ๋ 2.0 ๋ฒ์ ์ ๊ฐ๋ฐ ์ค์
๋๋ค.
-
์๋ฒ ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ: ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ์ ์ฅํ๊ณ , ์ฌ๋ฌ ๊ธฐ๊ธฐ ๊ฐ ๋๊ธฐํ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
-
์์ ๋คํธ์ํฌ ํ์ฅ: ์ฌ์ฉ์๊ฐ ์น๊ตฌ์ ๋ง์ง ์ ๋ณด๋ฅผ ๊ณต์ ํ๊ณ ์๋ก์ ์ปฌ๋ ์ ์ ํ์ํ ์ ์๋ ์์ ๊ธฐ๋ฅ์ ์ถ๊ฐํฉ๋๋ค.
-
AI ์ถ์ฒ ๊ธฐ๋ฅ ๊ฐํ: ์ฌ์ฉ์ ๋ฆฌ๋ทฐ ๋ฐ ๋ฐฉ๋ฌธ ๊ธฐ๋ก์ ๋ถ์ํ์ฌ ๊ฐ์ธํ๋ ๋ง์ง ์ถ์ฒ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
-
Framework
UIKit
,RXSwift
,RealmSwift
,MapKit
,Alamofire
,Kingfisher
,WebKit
,Zip
,PhotosUI
,SideMenu
,SnapKit
,MessageUI
,Cosmos
,Firebase Analytics
,Firebase Crashlytics
-
Design Pattern
MVVM
,MVC
-
1. ๋ง์ง์ ๊ธฐ๋กํ๊ณ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ
RealmSwift
๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์ฑ
1-1. Realm ๋ชจ๋ธ์ ๋ฆฌ๋ทฐ๋ก ์ ์ฅํ ๋ด์ฉ ์ ์
1-2. Realm Repository๋ฅผ ํ์ฉํ CRUD ๊ตฌํ
1-3. ๋ฐ์ดํฐ๋ฅผ ๋ณ์ ์, ์๊ฐ์, ๋ฐฉ๋ฌธ์ ์ ๋ ฌํ๋ ๊ธฐ๋ฅ -
2. ๋๋ง์ ๋ง์ง ์จ๋ฒ์ ๋ง๋ค์ด ์นดํ ๊ณ ๋ฆฌ๋ฅผ ๋ถ๋ฅํ๋ ๊ธฐ๋ฅ
2-1. To-Many Relationship์ ํ์ฉํ ์จ๋ฒ ์์ฑ ๊ธฐ๋ฅ
2-2. ์ฌ์ด๋ ๋ฉ๋ด๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ์นดํ ๊ณ ๋ฆฌ ํ์ ์ง์ -
3. ์์์ ๊ฒ์ ๊ธฐ๋ฅ
3-1. ์นด์นด์ค ๋ก์ปฌ API ํ์ฉ -
4. WebView๋ฅผ ์ฌ์ฉํ์ฌ ์์์ ์ฌ์ดํธ๋ก ๋ฐ๋ก ์ด๋ํ๋ ๊ธฐ๋ฅ
-
5. ์ง๋์์ ๋๋ง์ ๋ง์ง์ ๋ณผ ์ ์๋ ๊ธฐ๋ฅ
5-1. MapKit์ Annotation์ ํ์ฉํ์ฌ ์ง๋์ ํ์ ์ฌ์ง์ผ๋ก ํํ
5-2. ์ง๋์์ ํ์ ํด๋ฌ์คํฐ๋ง์ผ๋ก ๊ตฌํ
5-3. SearchBar๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฅํ ๋ง์ง์ ๊ฒ์ํ ์ ์๋ ๊ธฐ๋ฅ -
6. ๋ฐฑ์ ํ์ผ ์์ฑ ๋ฐ ๊ณต์ /๋ณต๊ตฌ ๊ธฐ๋ฅ
6-1. ๋ฐฑ์ ํ์ผ ์์ฑ ๋ฐ ZIP ํ์ผ๋ก ์์ถ
6-2. ๋ฐฑ์ ํ์ผ ๋ณต๊ตฌ ๋ฐ ZIP ํด์
6-3. ๋ฐฑ์ ํ์ผ ๊ณต์
6-4. ๋ฐฑ์ ํ์ผ ์ญ์ -
7. ์ฑ์์ ์ง์ ์ด๋ฉ์ผ์ ํตํด ๋ฌธ์๋ ์๊ฒฌ์ ์์งํ ์ ์๋ ๊ธฐ๋ฅ
7-1. ์ด๋ฉ์ผ ์์ฑ ๋ฐ ์ ์ก
7-2. ์ด๋ฉ์ผ ์ ์ก ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
์ด ๊ธฐ๋ฅ์ ์ฌ์ฉ์๊ฐ ๋ง์ง ๋ฆฌ๋ทฐ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ๊ธฐ๋กํ๊ณ , ๋ฐ์ดํฐ๋ฅผ ์ง๊ด์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค. RealmSwift ๊ธฐ๋ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง๊ด์ ์ธ ์ ๋ ฌ/๊ฒ์ ๊ธฐ๋ฅ์ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํ๋ฉฐ, ์จ๋ฒ ๊ด๋ฆฌ์ ์ด๋ฏธ์ง ํ์ผ ์ฒ๋ฆฌ ๋ฑ ์์ธํ ๊ธฐ๋ฅ์ ์ฑ์ ์ ์ฉ์ฑ์ ํ์ธต ๋ ๋์์ต๋๋ค.
- ๋ง์ง ๋ฆฌ๋ทฐ๋ฅผ ์ ์ฅํ ReviewTable๊ณผ ์จ๋ฒ ๊ด๋ฆฌ๋ฅผ ์ํ AlbumTable ํด๋์ค ์ ์
- ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ์๋ ๋ณ์ , ๋ฐฉ๋ฌธ ํ์, ๋ฉ๋ชจ, ์ด๋ฏธ์ง ๊ฒฝ๋ก, ์์น ์ ๋ณด(์๋/๊ฒฝ๋) ๋ฑ์ด ํฌํจ๋จ
- ์จ๋ฒ๊ณผ ๋ฆฌ๋ทฐ ๊ฐ์ To-Many ๊ด๊ณ๋ฅผ ์ค์ ํ์ฌ ์จ๋ฒ๋ณ ๋ฆฌ๋ทฐ๋ฅผ ๊ด๋ฆฌ
class ReviewTable: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var storeName: String
@Persisted var starCount: Double
@Persisted var reviewDate: Date
@Persisted var memo: String
@Persisted var imageView1URL: String? // ์ด๋ฏธ์ง ๊ฒฝ๋ก
@Persisted var visitCount: Int?
@Persisted var album: LinkingObjects<AlbumTable> = LinkingObjects(fromType: AlbumTable.self, property: "reviews")
}
class AlbumTable: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var albumName: String
@Persisted var reviews: List<ReviewTable> // To-Many ๊ด๊ณ
}
ReviewTableRepository๋ ๋ง์ง ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ ์ํด ์ค๊ณ๋์์ต๋๋ค. CRUD ์์
์ธ์๋ ์ด๋ฏธ์ง ํ์ผ ์ ์ฅ ๋ฐ ์ญ์ , ๋ฐ์ดํฐ ์ด๊ธฐํ์ ๊ฐ์ ์ ํธ๋ฆฌํฐ ๊ธฐ๋ฅ์ ํฌํจํ์ฌ ์ฌ์ฉ์์ ํธ๋ฆฌํ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ์ง์ํฉ๋๋ค. ์ด๋ฌํ ์ธ๋ถ ๊ตฌํ์ ์ฑ์ด ๋ฐ์ดํฐ๋ฅผ ์ ๋ขฐ์ฑ ์๊ฒ ์ฒ๋ฆฌํ๊ณ , ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐ ๊ธฐ์ฌํฉ๋๋ค.
- ์ฝ๊ธฐ(Read)
- ๋ชจ๋ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ฑฐ๋ ํน์ ์กฐ๊ฑด์ ๋ง๋ ๋ฐ์ดํฐ๋ฅผ ํํฐ๋ง
- ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฌ(๋ณ์ ์, ๋ฆฌ๋ทฐ ๋ ์ง์, ๋ฐฉ๋ฌธ ํ์์)ํ์ฌ ์ ๊ณต
- ์์ฑ ๋ฐ ์ ์ฅ (Create)
- ์ ๋ฆฌ๋ทฐ๋ฅผ ์ ์ฅํ๊ณ ์จ๋ฒ๊ณผ ์ฐ๊ณ
- ์์ (Update)
- ๊ธฐ์กด ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ์
๋ฐ์ดํธ
- ์ญ์ (Delete)
- ๋ฆฌ๋ทฐ ์ญ์ ์ ๊ด๋ จ ์ด๋ฏธ์ง ํ์ผ๋ ํจ๊ป ์ ๊ฑฐ
- ์ ํธ๋ฆฌํฐ ๊ธฐ๋ฅ
- ๋ฐ์ดํฐ ์ด๊ธฐํ, ์ด๋ฏธ์ง ์ ์ฅ/๊ด๋ฆฌ, ํ์ผ ๊ฒฝ๋ก ๊ฒ์ ๊ธฐ๋ฅ
class ReviewTableRepository: ReviewTableRepositoryType {
private let realm = try! Realm()
// **์ฝ๊ธฐ (Read)**
func fetch() -> Results<ReviewTable> {
return realm.objects(ReviewTable.self).sorted(byKeyPath: "reviewDate", ascending: false)
}
// **์์ฑ ๋ฐ ์ ์ฅ (Create)**
func saveReview(_ review: ReviewTable) {
try! realm.write {
realm.add(review)
}
}
// **์์ (Update)**
func updateReview(_ existingReview: ReviewTable, with updatedReview: ReviewTable) {
try! realm.write {
existingReview.starCount = updatedReview.starCount
existingReview.memo = updatedReview.memo
existingReview.reviewDate = updatedReview.reviewDate
existingReview.imageView1URL = updatedReview.imageView1URL
existingReview.visitCount = updatedReview.visitCount
}
}
// **์ญ์ (Delete)**
func deleteReview(_ review: ReviewTable) {
try! realm.write {
// ๋ฆฌ๋ทฐ ์ญ์ ์ ์ ๊ด๋ จ ์ด๋ฏธ์ง ์ ๊ฑฐ
if let imageURL = review.imageView1URL {
removeImageFromDocument(imageURL: imageURL)
}
realm.delete(review)
}
}
// **์ด๋ฏธ์ง ํ์ผ ์ ์ฅ**
func saveImageToDocument(fileName: String, image: UIImage) -> String? {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let fileURL = documentDirectory.appendingPathComponent(fileName)
guard let data = image.jpegData(compressionQuality: 0.5) else { return nil }
do {
try data.write(to: fileURL)
return fileURL.absoluteString
} catch {
print("Image save error: \(error)")
return nil
}
}
// **์ด๋ฏธ์ง ํ์ผ ์ญ์ **
func removeImageFromDocument(imageURL: String) {
if let filePath = URL(string: imageURL)?.path {
try? FileManager.default.removeItem(atPath: filePath)
}
}
// **๋ฐ์ดํฐ ์ด๊ธฐํ**
func clearAllData() {
try! realm.write {
realm.deleteAll()
}
}
}
- byKeyPath: starCount, reviewDate, visitCount ์
๋ ฅ
func fetchSortedReviews(by key: String, ascending: Bool) -> Results<ReviewTable> {
return realm.objects(ReviewTable.self).sorted(byKeyPath: key, ascending: ascending)
}
์ด ๊ธฐ๋ฅ์ ์ฌ์ฉ์๊ฐ ๊ฐ์ธ์ ์ผ๋ก ์์คํ ๋ง์ง ๋ฆฌ๋ทฐ๋ฅผ ์ ๋ฆฌํ๋ ๋ฐ ๊ฐ๋ ฅํ ๋๊ตฌ๋ฅผ ์ ๊ณตํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ฌ์ฉ์ ๋ง์กฑ๋๋ฅผ ๊ทน๋ํํ๊ณ ์นดํ ๊ณ ๋ฆฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ๋์ฑ ์ง๊ด์ ์ผ๋ก ๋ง๋ฆ์ผ๋ก์จ ์ฌ์ฉ์๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ํ์ํ๊ณ ๊ด๋ฆฌํ ์ ์๋๋ก ๋์ต๋๋ค.
- ์๋ก์ด AlbumTable ์ธ์คํด์ค๋ฅผ ์์ฑ
- Realm ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
- ์๋ก์ด ์จ๋ฒ์ด ์์ฑ๋๋ฉด ์ฌ์ด๋ ๋ฉ๋ด์ ์ฐ๋๋์ด UI์ ์ค์๊ฐ ๋ฐ์
class AlbumTable: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var albumName: String
@Persisted var reviews: List<ReviewTable> // To-Many Relationship
convenience init(albumName: String) {
self.init()
self.albumName = albumName
}
@objc func addAlbumButtonTapped() {
let alertController = UIAlertController(title: "์๋ก์ด ์จ๋ฒ", message: "์จ๋ฒ ์ด๋ฆ์ ์
๋ ฅํ์ธ์.", preferredStyle: .alert)
alertController.addTextField { textField in
textField.placeholder = "์จ๋ฒ ์ด๋ฆ"
}
let addAction = UIAlertAction(title: "์ถ๊ฐ", style: .default) { _ in
guard let albumName = alertController.textFields?.first?.text, !albumName.isEmpty else { return }
let newAlbum = AlbumTable(albumName: albumName)
try! self.realm.write {
self.realm.add(newAlbum)
}
self.sideMenuTableViewController.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "์ทจ์", style: .cancel)
alertController.addAction(addAction)
alertController.addAction(cancelAction)
present(alertController, animated: true)
}
SideMenu
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ ํฅ์- ์ฌ์ด๋ ๋ฉ๋ด์ ํ์๋๋ ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก(์จ๋ฒ ์ด๋ฆ)
- "+ ์จ๋ฒ ์ถ๊ฐ" ๋ฒํผ์ ํตํด ์๋ก์ด ์นดํ ๊ณ ๋ฆฌ ์์ฑ ๊ฐ๋ฅ
UITableView
๋ฅผ ์ฌ์ฉํ์ฌ ์จ๋ฒ ๋ชฉ๋ก ํ์
func setupSideMenu() {
sideMenuTableViewController = UITableViewController()
sideMenuTableViewController.tableView.delegate = self
sideMenuTableViewController.tableView.dataSource = self
sideMenu = SideMenuNavigationController(rootViewController: sideMenuTableViewController)
sideMenu?.leftSide = true
SideMenuManager.default.leftMenuNavigationController = sideMenu
}
- ์ ํํ ์จ๋ฒ์ ์ํ ๋ฆฌ๋ทฐ๋ง ํํฐ๋งํ์ฌ ํ์
- "๋ชจ๋ ๋ณด๊ธฐ"๋ฅผ ์ ํํ๋ฉด ๋ชจ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋ณด์ฌ์ค
UserDefaults
์Realm
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ํ์ฉํด ์ ํํ ์จ๋ฒ ์ํ๋ฅผ ์ ์ง
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedAlbum = albumNames[indexPath.row]
if selectedAlbum == "๋ชจ๋ ๋ณด๊ธฐ" {
reviewItems = repository.fetch()
} else if let matchingAlbum = realm.objects(AlbumTable.self).filter("albumName == %@", selectedAlbum).first {
reviewItems = repository.fetch().filter("ANY album._id == %@", matchingAlbum._id)
}
mainView.collectionView.reloadData()
sideMenu?.dismiss(animated: true)
}
์นด์นด์ค ๋ก์ปฌ API๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์ํ๋ ์์์ ์ ์์ฝ๊ฒ ๊ฒ์ํ ์ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค. ๊ฒ์๋ ์์์ ์ ์ด๋ฆ, ์ฃผ์, ์ ํ๋ฒํธ, ์นดํ
๊ณ ๋ฆฌ ์ ๋ณด, ์์น ์ขํ ๋ฑ์ ๋ฐ์์ ์ง๊ด์ ์ผ๋ก ์ ๋ณด๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ฌ์ฉ์๋ ํ์ํ ์ ๋ณด๋ฅผ ๋น ๋ฅด๊ฒ ํ์ธํ๊ณ , ์ฑ ๋ด์์ ํจ์จ์ ์ผ๋ก ์์์ ์ ํ์ํ ์ ์์ต๋๋ค.
Search.MP4
- ์นด์นด์ค ๋ก์ปฌ API ์ฐ๋
Alamofire
๋ฅผ ์ฌ์ฉํ์ฌ ์นด์นด์ค ๋ก์ปฌ API์ ํต์- ๊ฒ์์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก API์์ฒญ์ ๋ณด๋ด๊ณ , ์์์ ์ ๋ณด๋ฅผ JSON ํํ๋ก ๋ฐ์์ ๋์ฝ๋ฉ
- ๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
- ๊ฒ์ ๊ฒฐ๊ณผ๋ Document ๋ชจ๋ธ๋ก ๋์ฝ๋ฉํ์ฌ, ์์์ ์ด๋ฆ, ์ฃผ์, ์ ํ๋ฒํธ ๋ฑ ํ์ํ ์ ๋ณด๋ฅผ ๊ตฌ์กฐํ
- API ์์ฒญ ์ต์ ํ
- ๊ณตํต ํค๋ ๋ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ตฌ์ฑํ์ฌ ๊ฐ๊ฒฐํ ์ฝ๋ ์ ์ง
- ์ํ ์ฝ๋ ๊ฒ์ฆ ๋ฐ ์๋ฌ ํธ๋ค๋ง ์ถ๊ฐ๋ก ์์ ์ ์ธ ํต์ ๊ตฌํ
- ํ์ฅ ๊ฐ๋ฅ์ฑ ๊ณ ๋ ค
- API ์๋ต ๋ฐ์ดํฐ๋ responseDecodable์ ํ์ฉํ์ฌ Food์ Document ๊ตฌ์กฐ์ฒด๋ก ๋งคํ
- ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ ์ ํ์ฅ ๊ฐ๋ฅํ ํํ๋ก ์์ฑํ์ฌ, ์๋ก์ด ์๊ตฌ์ฌํญ์ ์ฝ๊ฒ ๋์ ๊ฐ๋ฅ
func searchPlaceCallRequest(query: String, page: Int = 1, size: Int = 45, completion: @escaping ([Document]?) -> Void) {
let baseURL = "https://dapi.kakao.com/v2/local/search/keyword.json"
let header: HTTPHeaders = [
"Authorization": "KakaoAK \(APIKey.key)",
"Content-Type": "application/json; charset=UTF-8"
]
let parameters: [String: Any] = ["query": query]
AF.request(baseURL, method: .get, parameters: parameters, headers: header)
.validate(statusCode: 200...500)
.responseDecodable(of: Food.self) { response in
switch response.result {
case .success(let value):
completion(value.documents) // ์ฑ๊ณต ์ ์์์ ๋ฐ์ดํฐ ๋ฐํ
case .failure(let error):
print("Error: \(error)") // ์คํจ ์ ์๋ฌ ๋ก๊ทธ ์ถ๋ ฅ
completion(nil)
}
}
}
- ์ฌ์ฉ๋ ๋ฐ์ดํฐ ๋ชจ๋ธ
struct Food: Decodable {
let documents: [Document]
}
struct Document: Decodable {
let addressName: String?
let categoryName: String?
let phone: String?
let placeName: String?
let placeURL: String?
let roadAddressName: String?
let x: String? // ๊ฒฝ๋
let y: String? // ์๋
var finalCategory: String? {
return categoryName?.split(separator: ">").last?.trimmingCharacters(in: .whitespaces)
}
}
WebView ๊ธฐ๋ฅ์ WebKit
์ ํ์ฉํ์ฌ ๋ง์ง ๋ฆฌ๋ทฐ์ ๊ด๋ จ๋ ์ธ๋ถ ์ ๋ณด๋ฅผ ์ฝ๊ฒ ์ ๊ทผํ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค. ์ด๋ฅผ ํตํด ์ฌ์ฉ์๋ ๋ณ๋์ ๋ธ๋ผ์ฐ์ ์์ด๋ ์ฑ ๋ด์์ ๋งํฌ๋ ์ฌ์ดํธ๋ฅผ ํ์ํ ์ ์์ต๋๋ค.
WebView.MP4
WKWebView
๋WebKit
ํ๋ ์์ํฌ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ธ๋ถ ์น ํ์ด์ง๋ฅผ ์ฑ ๋ด๋ถ์์ ๋ก๋ํ๋ ๋ฐ ์ฌ์ฉ- ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ์์ ์์์ ์ URL์ placeURL ๋ณ์์ ์ ์ฅ
- internetButton ํด๋ฆญ ์ openWebView ๋ฉ์๋ ์คํ
- URL ์ ํจ์ฑ ๊ฒ์ฌ ํ WebViewController๋ฅผ ์์ฑํ์ฌ ํ๋ฉด ์ ํ
class WebViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
var urlToLoad: URL
init(url: URL) {
self.urlToLoad = url
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// WebView ์ค์
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view.addSubview(webView)
// URL ์์ฒญ ๋ฐ ๋ก๋
let request = URLRequest(url: urlToLoad)
webView.load(request)
}
}
@objc func openWebView() {
// URL ์ ํจ์ฑ ๊ฒ์ฌ
guard var urlString = placeURL else { return }
if urlString.starts(with: "http://") {
urlString = urlString.replacingOccurrences(of: "http://", with: "https://")
}
guard let url = URL(string: urlString) else { return }
// WebViewController๋ก ์ ํ
let webVC = WebViewController(url: url)
present(webVC, animated: true, completion: nil)
}
์ฌ์ฉ์๊ฐ ์ ์ฅํ ๋ง์ง ์ ๋ณด๋ฅผ ์ง๋์ ํ์ํ์ฌ ์๊ฐ์ ์ผ๋ก ํํํ๊ณ , ๊ฒ์ ๊ธฐ๋ฅ์ ํตํด ๋ง์ง์ ์ฝ๊ฒ ํ์ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ค๋ช
ํฉ๋๋ค.
- MapKit ํ์ฉ: MKMapView์ MKAnnotation์ ์ฌ์ฉํ์ฌ ๋ง์ง ์ ๋ณด๋ฅผ ์ง๋์ ํ์
- Custom Annotation View: ๋ง์ง์ ๋ํ ์ด๋ฏธ์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด MKAnnotationView๋ฅผ ์ปค์คํฐ๋ง์ด์งํ์ฌ ImageAnnotationView๋ฅผ ๊ตฌํ
func loadAnnotations() {
let reviews = reviewRepository.fetch()
var newAnnotations: [MKPointAnnotation] = []
for review in reviews {
if let latitude = Double(review.latitude ?? ""), let longitude = Double(review.longitude ?? "") {
let annotation = MKPointAnnotation()
annotation.title = review.storeName
annotation.subtitle = review.imageView1URL
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
newAnnotations.append(annotation)
}
}
mapView.addAnnotations(newAnnotations)
}
- ํด๋ฌ์คํฐ๋ง ์ง์: ์ฌ๋ฌ ํ์ด ๋ชจ์ฌ ์์ ๊ฒฝ์ฐ, ImageClusterView๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ฌ์คํฐ๋ง๋ ํ๊ณผ ๋ง์ง ๊ฐ์๋ฅผ ์๊ฐ์ ์ผ๋ก ํํ
class ImageClusterView: MKAnnotationView {
private var imageView: UIImageView!
private var countLabel: UILabel!
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.frame = CGRect(x: 0, y: 0, width: 58, height: 58)
self.backgroundColor = .clear
imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = 10
imageView.clipsToBounds = true
self.addSubview(imageView)
countLabel = UILabel()
countLabel.textAlignment = .center
countLabel.textColor = .white
countLabel.backgroundColor = UIColor(named: "countGold")
countLabel.layer.cornerRadius = 10
countLabel.clipsToBounds = true
self.addSubview(countLabel)
countLabel.snp.makeConstraints { make in
make.top.equalToSuperview().inset(2)
make.right.equalToSuperview().inset(2)
make.width.height.equalTo(20)
}
}
override var annotation: MKAnnotation? {
willSet {
if let cluster = newValue as? MKClusterAnnotation {
countLabel.text = "\(cluster.memberAnnotations.count)"
if let firstAnnotation = cluster.memberAnnotations.first as? MKPointAnnotation,
let imageUrlString = firstAnnotation.subtitle,
let imageUrl = URL(string: imageUrlString) {
imageView.kf.setImage(with: imageUrl) // ๋ํ ์ด๋ฏธ์ง ๋ก๋
}
}
}
}
}
UISearchBar๋ฅผ ์ด์ฉํด ์ง๋ ์์ ํ์๋์ด ์๋ ๋ง์ง์ ์์์ ์ ์ด๋ฆ๊ณผ ๋ฉ๋ชจ์ฅ์ ์ ํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ๋น ๋ฅด๊ฒ ์ ์ฅ๋ ๋ง์ง์ ์ฐพ์ ์ ์์ต๋๋ค.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard !searchText.isEmpty else { return }
let storeNamePredicate = NSPredicate(format: "storeName CONTAINS[c] %@", searchText)
let memoPredicate = NSPredicate(format: "memo CONTAINS[c] %@", searchText)
let combinedPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [storeNamePredicate, memoPredicate])
let results = reviewRepository.fetch().filter(combinedPredicate)
guard let firstResult = results.first,
let latitude = Double(firstResult.latitude ?? ""),
let longitude = Double(firstResult.longitude ?? "") else { return }
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
mapView.setRegion(MKCoordinateRegion(center: coordinate, latitudinalMeters: 500, longitudinalMeters: 500), animated: true)
}
๋ง์๋ญ์ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๋ณด๊ดํ๊ณ , ๋ค๋ฅธ ๋๋ฐ์ด์ค๋ก ์ฝ๊ฒ ๋ณต๊ตฌํ ์ ์๋๋ก ๋ฐฑ์
๋ฐ ๋ณต๊ตฌ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
๋ฐฑ์
์ ์ฑ ๋ด๋ถ ๋ฐ์ดํฐ(default.realm)๋ฅผ ์์ถํ์ฌ ZIP ํ์ผ๋ก ์ ์ฅํ๋ฉฐ, ๊ณต์ ์ ๋ณต๊ตฌ๊ฐ ์ฉ์ดํฉ๋๋ค.
default.MP4
- ์ฑ ๋ด๋ถ ๋ฐ์ดํฐ(default.realm)๋ฅผ ZIP ํ์ผ๋ก ์์ถ
- ํ์ฌ ๋ ์ง์ ์๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ผ๋ช ์์ฑ
@objc func backupButtonTapped() {
Analytics.logEvent("backup_initiated", parameters: nil)
var urlPaths = [URL]()
guard let path = documentDirectoryPath() else {
print("๋ํ๋จผํธ ์์น์ ์ค๋ฅ๊ฐ ์์ต๋๋ค.")
return
}
let realmFile = path.appendingPathComponent("default.realm")
guard FileManager.default.fileExists(atPath: realmFile.path) else {
print("๋ฐฑ์
ํ ํ์ผ์ด ์์ต๋๋ค.")
return
}
urlPaths.append(realmFile)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy.MM.dd_HH:mm:ss"
let currentDateTime = dateFormatter.string(from: Date())
let fileName = "matchelin_\(currentDateTime)"
do {
let zipFilePath = try Zip.quickZipFiles(urlPaths, fileName: fileName)
print("Backup location: \(zipFilePath)")
backUpView.backupTableView.reloadData()
} catch {
print("๋ฐฑ์
์คํจ: \(error)")
}
}
- ์ฌ์ฉ์๋ก๋ถํฐ ZIP ํ์ผ์ ์ ํ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ณต๊ตฌ
- ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๋ฎ์ด์ฐ๊ธฐ ์ ์ฌ์ฉ์์๊ฒ ๊ฒฝ๊ณ ๋ฉ์์ง๋ฅผ ํ์
@objc func restoreButtonTapped() {
Analytics.logEvent("restore_initiated", parameters: nil)
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.archive], asCopy: true)
documentPicker.delegate = self
present(documentPicker, animated: true)
}
func unzipAndRestore(fileURL: URL) {
guard let destinationPath = documentDirectoryPath() else {
print("๋ณต๊ตฌ ๊ฒฝ๋ก ์ค๋ฅ")
return
}
do {
try Zip.unzipFile(fileURL, destination: destinationPath, overwrite: true, password: nil)
print("๋ณต๊ตฌ ์๋ฃ!")
} catch {
print("๋ณต๊ตฌ ์คํจ: \(error)")
}
}
- ZIP ํ์ผ์ ๋ค๋ฅธ ์ฑ์ด๋ ํด๋ผ์ฐ๋ ์๋น์ค๋ก ๊ณต์
func showActivityViewController(fileName: String) {
guard let path = documentDirectoryPath() else {
print("๋ํ๋จผํธ ์์น์ ์ค๋ฅ๊ฐ ์์ต๋๋ค.")
return
}
let backupFileURL = path.appendingPathComponent(fileName)
let activityVC = UIActivityViewController(activityItems: [backupFileURL], applicationActivities: nil)
present(activityVC, animated: true)
}
- ํ ์ด๋ธ ๋ทฐ์์ ์ฌ๋ผ์ด๋ ๋์์ผ๋ก ๋ฐฑ์ ํ์ผ ์ญ์
- ์ญ์ ์ ์ฌ์ฉ์ ํ์ธ ๋ํ ์์ ํ์
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let fileName = fetchZipList()[indexPath.row].name
Analytics.logEvent("backup_file_deleted", parameters: ["fileName": fileName])
guard let path = documentDirectoryPath() else { return }
let fileURL = path.appendingPathComponent(fileName)
do {
try FileManager.default.removeItem(at: fileURL)
tableView.deleteRows(at: [indexPath], with: .fade)
} catch {
print("ํ์ผ ์ญ์ ์คํจ: \(error)")
}
}
}
๋ง์๋ญ์ ์ฌ์ฉ์์ ํผ๋๋ฐฑ์ ์์งํ๊ธฐ ์ํด ์ด๋ฉ์ผ์ ํตํ ๊ฐํธํ ๋ฌธ์/์๊ฒฌ ์์ง ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ MessageUI
ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ฑ ๋ด๋ถ์์ ์ด๋ฉ์ผ์ ์์ฑํ๊ณ ๋ณด๋ผ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค.
Mail.MP4
- ์ด๋ฉ์ผ ์์ฑ ๋ฐ ์ ์ก์ MFMailComposeViewController๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํ
- ๋๋ฐ์ด์ค ๋ชจ๋ธ, ๋๋ฐ์ด์ค OS ๋ฒ์ , ์ฑ ๋ฒ์ ์ ๊ธฐ๊ธฐ์์ ์ ๋ ฅ์์ด ์๋์ผ๋ก ๊ฐ์ ธ์ต๋๋ค.
func sendEmail() {
let bodyString = """
๋ฌธ์ ์ฌํญ ๋ฐ ์๊ฒฌ์ ์์ฑํด์ฃผ์ธ์.
-------------------
Device Model : \(Utils.getDeviceModelName())
Device OS : \(UIDevice.current.systemVersion)
App Version : \(Utils.getAppVersion())
-------------------
"""
if MFMailComposeViewController.canSendMail() {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["[email protected]"]) // ๋ฌธ์ ์ด๋ฉ์ผ ์ฃผ์
mail.setSubject("๋ง์๋ญ / ๋ฌธ์,์๊ฒฌ")
mail.setMessageBody(bodyString, isHTML: false)
present(mail, animated: true)
} else {
// ์ด๋ฉ์ผ ์ค์ ์ด ๋์ด ์์ง ์์ ๊ฒฝ์ฐ
let alert = UIAlertController(title: "์ด๋ฉ์ผ ์ ์ก ๋ถ๊ฐ", message: "์ด๋ฉ์ผ ๊ณ์ ์ค์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ํ์ธ", style: .default))
present(alert, animated: true)
}
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
isEmailBeingSent = false
switch result {
case .sent:
print("์ด๋ฉ์ผ ์ ์ก ์๋ฃ")
case .cancelled:
print("์ด๋ฉ์ผ ์ ์ก ์ทจ์")
case .failed:
print("์ด๋ฉ์ผ ์ ์ก ์คํจ")
case .saved:
print("์ด๋ฉ์ผ ์์ ์ ์ฅ")
@unknown default:
break
}
controller.dismiss(animated: true)
}
๋ง์๋ญ 2.0์ ํ์ ๊ด๋ฆฌ, ๊ฒ์๊ธ ์์ฑ, ๋๊ธ, ํ๋กํ ๊ด๋ฆฌ์ ๊ฐ์ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ฌ์ฉ์ ๊ฒฝํ์ ๊ทน๋ํํฉ๋๋ค. ์ด ๊ณผ์ ์์ MVVM ๋์์ธ ํจํด, Alamofire
, RxSwift
, ๊ทธ๋ฆฌ๊ณ ํ๋กํ ์ฝ ์งํฅ ์ค๊ณ๋ฅผ ํ์ฉํ์ฌ ํจ์จ์ ์ด๊ณ ์ ์ง๋ณด์ ๊ฐ๋ฅํ ๋คํธ์ํฌ ๊ณ์ธต์ ๊ตฌ์ฑํ์ต๋๋ค. ์๋๋ ์ด ํ๋ก์ ํธ์ ํต์ฌ ์ค๊ณ์ ๊ตฌํ ๋ฐฉ์์
๋๋ค.
-
1. MVVM ๋์์ธ ํจํด
-
2. RxSwift์ Alamofire๋ฅผ ํ์ฉํ ๋น๋๊ธฐ ๋คํธ์ํฌ ์ฒ๋ฆฌ
-
3. ๊ณตํต ๋ก์ง๊ณผ ํ๋กํ ์ฝ์ ํ์ฉํ ๋คํธ์ํฌ ๊ณ์ธต ์ค๊ณ
-
4. ์ํ ์ฝ๋์ ์๋ฌ ์ฒ๋ฆฌ
<์ฑ๊ณผ>
- ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ ๋ฐ ํ์ฅ์ฑ ํฅ์: ๊ณตํต ๋ก์ง๊ณผ ํ๋กํ ์ฝ ์ค๊ณ๋ฅผ ํตํด API ์ถ๊ฐ๊ฐ ์ฉ์ด
- ํจ์จ์ ์ธ ๋น๋๊ธฐ ์ฒ๋ฆฌ: RxSwift๋ฅผ ์ฌ์ฉํ MVVM ์ํคํ ์ฒ๋ก ๋ฐ์ดํฐ ํ๋ฆ ๊ด๋ฆฌ
- ์ ์ง๋ณด์์ฑ ๊ฐํ: ํ๋กํ ์ฝ๊ณผ ๊ณตํต ๋ก์ง ๋ถ๋ฆฌ๋ก ์ฝ๋ ์์ ๋ฐ ๊ธฐ๋ฅ ์ถ๊ฐ ๋น์ฉ ๊ฐ์
- ํ ์คํธ ๊ฐ๋ฅ์ฑ: ๋คํธ์ํฌ ๊ณ์ธต ์ถ์ํ๋ฅผ ํตํด ๋จ์ ํ ์คํธ ๊ฐ๋ฅ
- ๋ง์๋ญ 2.0์ ์ค๊ณ๋ ํด๋ฆฐ ์ฝ๋์ ์ ์ง๋ณด์์ฑ์ ์ค์ํ๋ฉฐ, ์ฌ์ฉ์์ ๊ฐ๋ฐ์๊ฐ ๋ชจ๋ ๋ง์กฑํ ์ ์๋ ์๋น์ค๋ฅผ ์ ๊ณตํฉ๋๋ค.
- ๋ง์๋ญ 2.0์ Model-View-ViewModel(MVVM) ํจํด์ ์ ์ฉํ์ฌ ๋ก์ง๊ณผ UI๋ฅผ ๋ช ํํ ๋ถ๋ฆฌ
- Model: ๋ฐ์ดํฐ์ API ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋คํธ์ํฌ ๊ณ์ธต์ ํฌํจ
- ViewModel: ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๋นํ๋ฉฐ, RxSwift๋ฅผ ์ฌ์ฉํด View์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉ
- View: ์ฌ์ฉ์ ์ธํฐํ์ด์ค(UI)๋ฅผ ๋ด๋นํ๋ฉฐ, ViewModel์ ์ํ ๋ณํ๋ฅผ ๊ตฌ๋ ํ์ฌ UI๋ฅผ ์ ๋ฐ์ดํธ
- Alamofire๋ฅผ ํตํด API ์์ฒญ์ ์ฒ๋ฆฌํ๊ณ , RxSwift๋ก ๊ฒฐ๊ณผ๋ฅผ Observable๋ก ๋ณํํ์ฌ ๋ฐ์ดํฐ ํ๋ฆ์ ๊ฐ๊ฒฐํ๊ฒ ๊ด๋ฆฌ
- ๋คํธ์ํฌ ์์ฒญ์ ์ฑ๊ณต/์คํจ ์ํ๋ฅผ RxSwift์ PublishSubject๋ก View์ ์ ๋ฌ
- RxSwift๋ก ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉํ์ฌ ๋น๋๊ธฐ ์์ ์ ๋ณต์ก์ฑ์ ๊ฐ์
class LoginViewModel {
private var networkManager: NetworkManagerProtocol
private let disposeBag = DisposeBag()
// RxSwift Subjects
let loginSuccess = PublishSubject<Void>()
let loginFailure = PublishSubject<String>()
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
func login(email: String, password: String) {
Observable.create { [weak self] observer in
self?.networkManager.login(email: email, password: password) { success, accessToken, refreshToken, message in
if success {
UserDefaults.standard.set(accessToken, forKey: "accessToken")
UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
observer.onNext(())
observer.onCompleted()
} else {
observer.onError(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: message ?? "Login failed"]))
}
}
return Disposables.create()
}
.subscribe(
onNext: { [weak self] _ in self?.loginSuccess.onNext(()) },
onError: { [weak self] error in self?.loginFailure.onNext(error.localizedDescription) }
)
.disposed(by: disposeBag)
}
}
- View๋ ViewModel์ ์ํ๋ฅผ ๊ตฌ๋ ํ์ฌ UI๋ฅผ ์ ๋ฐ์ดํธ
private func setupViewModelBindings() {
viewModel.loginSuccess
.subscribe(onNext: { [weak self] _ in self?.navigateToMain() })
.disposed(by: disposeBag)
viewModel.loginFailure
.subscribe(onNext: { [weak self] message in self?.showAlert(with: message, navigateToMain: false) })
.disposed(by: disposeBag)
}
NetworkManagerProtocol๊ณผ ๋ค์ํ API ์์ฒญ ๊ตฌ์กฐ์ฒด(SignUpRequest, LoginRequest)๋ฅผ ์ ์ํ์ฌ ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ๊ณผ ํ
์คํธ ๊ฐ๋ฅ์ฑ์ ๋์์ต๋๋ค.
- NetworkManagerProtocol ์ ์
- APIRequest ํ๋กํ ์ฝ ์ ์
- ํ๋กํ ์ฝ์ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ๋ณ API ์์ฒญ ๊ตฌ์กฐ์ฒด๋ฅผ ์ ์ํ์ฌ ํ์ฅ์ฑ ๊ฐํ
protocol NetworkManagerProtocol {
func signUp(email: String, password: String, nick: String, phoneNum: String?, birthDay: String?, completion: @escaping (Bool, String?, Int?) -> Void)
func validateEmail(email: String, completion: @escaping (Bool, String?, Int?) -> Void)
func login(email: String, password: String, completion: @escaping (Bool, String?, String?, String?) -> Void)
func refreshToken(completion: @escaping (Bool, String?) -> Void)
func withdraw(accessToken: String, completion: @escaping (Bool, String?) -> Void)
}
protocol APIRequest {
var method: HTTPMethod { get }
var path: String { get }
var parameters: Parameters? { get }
var headers: HTTPHeaders { get }
}
-----------------------------------------------------------------------------------------
struct SignUpRequest: APIRequest {
let email: String
let password: String
let nick: String
var method: HTTPMethod { .post }
var path: String { "/join" }
var parameters: Parameters? { ["email": email, "password": password, "nick": nick] }
var headers: HTTPHeaders { ["Content-Type": "application/json"] }
}
- ๊ณตํต๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ๋ชจ๋ API ์์ฒญ๊ณผ ์ํ ์ฝ๋๋ฅผ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ์ค๋ณต ์ฝ๋๋ฅผ ์ต์ํ
// MARK: - ๊ณตํต๋ก์ง
private func performRequest(_ request: APIRequest, completion: @escaping (AFDataResponse<Any>, Error?) -> Void) {
do {
let urlRequest = try request.asURLRequest(baseURL: baseURL)
AF.request(urlRequest).validate().responseJSON { response in
completion(response, response.error)
}
} catch {
completion(AFDataResponse<Any>(request: nil, response: nil, data: nil, metrics: nil, serializationDuration: 0, result: .failure(error as! AFError)), error)
}
}
- ์๋ฒ์์ ๋ฐํ๋ ์ํ ์ฝ๋๋ฅผ ๊ณตํต์ ์ผ๋ก ์ฒ๋ฆฌํ์ฌ ํด๋ผ์ด์ธํธ์์ ๋์ผํ ๋ฐฉ์์ผ๋ก ์๋ฌ๋ฅผ ๊ด๋ฆฌ
// MARK: - ์ํ์ฝ๋ ๊ณตํต๋ก์ง
private func processResponse(_ response: AFDataResponse<Any>, completion: (Bool, String?, Int?) -> Void) {
guard let statusCode = response.response?.statusCode else {
print("์๋ต ์ฝ๋๋ฅผ ๋ฐ์ง ๋ชปํ์ต๋๋ค.")
completion(false, "์๋ต ์ฝ๋๋ฅผ ๋ฐ์ง ๋ชปํ์ต๋๋ค.", nil)
return
}
let message = parseResponseMessage(response.data)
print("์ํ ์ฝ๋: \(statusCode), ๋ฉ์์ง: \(message ?? "๋ฉ์์ง ์์")")
switch statusCode {
case 200:
completion(true, "์ฑ๊ณต", statusCode)
case 400:
completion(false, "ํ์ ๊ฐ์ด ๋๋ฝ๋์ต๋๋ค", statusCode)
case 401:
completion(false, "์ ๊ทผ์ด ๊ฑฐ๋ถ๋์์ต๋๋ค", statusCode)
case 403:
completion(false, "๊ธ์ง๋ ์ ๊ทผ์
๋๋ค", statusCode)
case 409:
completion(false, "์ด๋ฏธ ๊ฐ์
ํ ์ ์ ์
๋๋ค.", statusCode)
case 418:
completion(false, "๋ฆฌํ๋ ์ ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์", statusCode)
case 419:
completion(false, "์ก์ธ์ค ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค", statusCode)
case 420:
completion(false, "์๋ชป๋ SesacKey์
๋๋ค", statusCode)
case 429:
completion(false, "์์ฒญ์ด ๋๋ฌด ๋ง์ต๋๋ค", statusCode)
case 444:
completion(false, "์๋ชป๋ URL์
๋๋ค", statusCode)
case 500:
completion(false, "์๋ฒ ์๋ฌ", statusCode)
default:
completion(false, message ?? "์ ์ ์๋ ์๋ฌ", statusCode)
}
}
ยฉ 2024 ์ด์นํ (SeungHyeon Lee). All rights reserved.
-
๋ณธ GitHub ๋ฆฌ๋๋ฏธ๋ ๊ฐ์ธ ํ๋ก์ ํธ ๋ง์๋ญ์ ์๊ฐํ๊ธฐ ์ํด ์์ฑ๋์์ผ๋ฉฐ, ๋ฆฌ๋๋ฏธ์ ๋ชจ๋ ๋ด์ฉ์ ์ด์นํ(SeungHyeon Lee)์ด ์ง์ ์์ฑํ์์ต๋๋ค.
-
๋ง์๋ญ์ ๊ธฐํ, ๋์์ธ, ๊ฐ๋ฐ, ๋ฐฐํฌ๊น์ง ์ ๊ณผ์ ์ ์ ๊ฐ ๋จ๋ ์ผ๋ก ์ํํ ๊ฐ์ธ ํ๋ก์ ํธ์ ๋๋ค. ๋ฆฌ๋๋ฏธ์ ํฌํจ๋ ๋ชจ๋ ํ ์คํธ, ์ด๋ฏธ์ง ๋ฐฐ์น, ์ค๋ช , ๊ธฐ์ ์คํ ์๊ฐ ๋ฑ์ ์ ์ ์์ ์์ ๋ฐํ๋๋ค.
-
๋ฆฌ๋๋ฏธ์ ๊ด๋ จํ์ฌ ๋ฌธ์๊ฐ ํ์ํ์ ๊ฒฝ์ฐ ์๋ ์ด๋ฉ์ผ๋ก ์ฐ๋ฝํด ์ฃผ์ธ์: [email protected]