Skip to content

Commit b020307

Browse files
committed
💻 샘플 프로젝트 7개 완성 + DocC 41개 (82%)
샘플 프로젝트 (7개): - TaskMaster (SwiftData) - PlaceExplorer (MapKit) - MusicPlayer (MusicKit) - CartFlow (Observation) - SecureVault (LocalAuth) - NotifyMe (UserNotifications) - WeatherWidget (WidgetKit) DocC 튜토리얼 추가: - ImagePlayground, CoreBluetooth, MultipeerConnectivity, CoreNFC - SpriteKit, RealityKit, AVKit, CoreImage - iOS 26 신규 프레임워크 8개 진행률: - 📝 블로그: 100% - 📚 DocC: 82% (41/50) - 💻 샘플: 14% (7/50)
1 parent 2fc5107 commit b020307

436 files changed

Lines changed: 26717 additions & 145 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Apple의 **367개 프레임워크** 중 핵심 50개를 실전 중심으로 학
2121
| 구분 | 완료 | 작업 중 | 미완성 | 진행률 |
2222
|------|------|---------|--------|--------|
2323
| 📝 블로그 | 50/50 | - | 완료! | 100% 🎉 |
24-
| 📚 DocC | 33/50 | - | 17개 | 66% |
25-
| 💻 샘플 | 1/50 | 29개 | 20개 | 2% |
24+
| 📚 DocC | 41/50 | - | 9개 | 82% |
25+
| 💻 샘플 | 7/50 | - | 43개 | 14% |
2626

2727
> **블로그 상태**: ✅ 완성 50개 (WidgetKit, ActivityKit, AppIntents, SwiftUI, SwiftData, Observation, StoreKit, FoundationModels, PassKit, CloudKit, AuthServices, HealthKit, WeatherKit, MapKit, CoreLocation, CoreML, Vision, UserNotifications, TipKit, SharePlay, ARKit, RealityKit, SpriteKit, CoreImage, PencilKit, PDFKit, AVFoundation, AVKit, MusicKit, PhotosUI, CoreHaptics, ShazamKit, ImagePlayground, CoreBluetooth, CoreNFC, MultipeerConnectivity, Network, LocalAuthentication, CryptoKit, CallKit, EventKit, Contacts, WiFiAware, VisualIntelligence, AlarmKit, EnergyKit, PermissionKit, RelevanceKit, ExtensibleImage, AccessorySetupKit) 🎊
2828
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
3+
// MARK: - 카트 아이템 모델
4+
/// 장바구니에 담긴 상품과 수량을 나타내는 모델
5+
6+
struct CartItem: Identifiable, Equatable {
7+
let id: UUID
8+
let product: Product
9+
var quantity: Int
10+
11+
init(id: UUID = UUID(), product: Product, quantity: Int = 1) {
12+
self.id = id
13+
self.product = product
14+
self.quantity = max(1, quantity) // 최소 1개
15+
}
16+
17+
// MARK: - 계산 속성
18+
19+
/// 해당 아이템의 총 금액 (상품 가격 × 수량)
20+
var totalPrice: Int {
21+
product.price * quantity
22+
}
23+
24+
/// 포맷팅된 총 금액 문자열
25+
var formattedTotalPrice: String {
26+
let formatter = NumberFormatter()
27+
formatter.numberStyle = .decimal
28+
let formatted = formatter.string(from: NSNumber(value: totalPrice)) ?? "\(totalPrice)"
29+
return "\(formatted)"
30+
}
31+
32+
// MARK: - Equatable
33+
34+
static func == (lhs: CartItem, rhs: CartItem) -> Bool {
35+
lhs.id == rhs.id && lhs.quantity == rhs.quantity
36+
}
37+
}
38+
39+
// MARK: - Preview / Mock Data
40+
41+
extension CartItem {
42+
static let preview = CartItem(
43+
product: .preview,
44+
quantity: 2
45+
)
46+
47+
static let samples: [CartItem] = [
48+
CartItem(product: Product.samples[0], quantity: 1),
49+
CartItem(product: Product.samples[3], quantity: 2),
50+
CartItem(product: Product.samples[5], quantity: 3),
51+
]
52+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import Foundation
2+
import Observation
3+
4+
// MARK: - CartStore (@Observable)
5+
/// iOS 17+ Observation 프레임워크를 사용한 카트 상태 관리
6+
///
7+
/// ## @Observable vs ObservableObject 비교
8+
///
9+
/// ### 기존 방식 (ObservableObject):
10+
/// ```swift
11+
/// class CartStore: ObservableObject {
12+
/// @Published var items: [CartItem] = []
13+
/// @Published var isLoading = false
14+
/// }
15+
/// ```
16+
/// - View에서 `@ObservedObject` 또는 `@StateObject` 필요
17+
/// - 모든 @Published 변경 시 전체 View 업데이트
18+
///
19+
/// ### 새로운 방식 (@Observable):
20+
/// ```swift
21+
/// @Observable
22+
/// class CartStore {
23+
/// var items: [CartItem] = []
24+
/// var isLoading = false
25+
/// }
26+
/// ```
27+
/// - View에서 별도 프로퍼티 래퍼 불필요 (자동 추적)
28+
/// - 실제 사용하는 프로퍼티만 추적하여 성능 최적화
29+
/// - Macro 기반으로 보일러플레이트 제거
30+
31+
@Observable
32+
class CartStore {
33+
// MARK: - 상태 (자동 추적됨)
34+
35+
/// 장바구니 아이템 목록
36+
var items: [CartItem] = []
37+
38+
/// 로딩 상태
39+
var isLoading = false
40+
41+
/// 결제 완료 상태
42+
var isCheckoutComplete = false
43+
44+
/// 오류 메시지
45+
var errorMessage: String?
46+
47+
// MARK: - 계산 속성
48+
49+
/// 카트 내 총 아이템 수량
50+
var totalItemCount: Int {
51+
items.reduce(0) { $0 + $1.quantity }
52+
}
53+
54+
/// 카트 총 금액
55+
var totalPrice: Int {
56+
items.reduce(0) { $0 + $1.totalPrice }
57+
}
58+
59+
/// 포맷팅된 총 금액
60+
var formattedTotalPrice: String {
61+
let formatter = NumberFormatter()
62+
formatter.numberStyle = .decimal
63+
let formatted = formatter.string(from: NSNumber(value: totalPrice)) ?? "\(totalPrice)"
64+
return "\(formatted)"
65+
}
66+
67+
/// 카트가 비어있는지 여부
68+
var isEmpty: Bool {
69+
items.isEmpty
70+
}
71+
72+
// MARK: - 카트 조작 메서드
73+
74+
/// 상품을 카트에 추가
75+
/// - Parameters:
76+
/// - product: 추가할 상품
77+
/// - quantity: 수량 (기본값: 1)
78+
func addToCart(_ product: Product, quantity: Int = 1) {
79+
// 이미 카트에 있는 상품이면 수량 증가
80+
if let index = items.firstIndex(where: { $0.product.id == product.id }) {
81+
items[index].quantity += quantity
82+
} else {
83+
// 새 상품이면 추가
84+
let newItem = CartItem(product: product, quantity: quantity)
85+
items.append(newItem)
86+
}
87+
}
88+
89+
/// 카트에서 상품 제거
90+
/// - Parameter product: 제거할 상품
91+
func removeFromCart(_ product: Product) {
92+
items.removeAll { $0.product.id == product.id }
93+
}
94+
95+
/// 특정 아이템의 수량 변경
96+
/// - Parameters:
97+
/// - item: 대상 카트 아이템
98+
/// - newQuantity: 새 수량 (0 이하면 제거)
99+
func updateQuantity(for item: CartItem, to newQuantity: Int) {
100+
guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
101+
102+
if newQuantity <= 0 {
103+
items.remove(at: index)
104+
} else {
105+
items[index].quantity = newQuantity
106+
}
107+
}
108+
109+
/// 수량 1 증가
110+
func incrementQuantity(for item: CartItem) {
111+
updateQuantity(for: item, to: item.quantity + 1)
112+
}
113+
114+
/// 수량 1 감소 (1개면 제거)
115+
func decrementQuantity(for item: CartItem) {
116+
updateQuantity(for: item, to: item.quantity - 1)
117+
}
118+
119+
/// 카트 비우기
120+
func clearCart() {
121+
items.removeAll()
122+
isCheckoutComplete = false
123+
errorMessage = nil
124+
}
125+
126+
/// 특정 상품이 카트에 있는지 확인
127+
func contains(_ product: Product) -> Bool {
128+
items.contains { $0.product.id == product.id }
129+
}
130+
131+
/// 특정 상품의 카트 내 수량
132+
func quantity(of product: Product) -> Int {
133+
items.first { $0.product.id == product.id }?.quantity ?? 0
134+
}
135+
136+
// MARK: - 결제 시뮬레이션
137+
138+
/// 결제 처리 (Mock)
139+
@MainActor
140+
func checkout() async {
141+
guard !isEmpty else { return }
142+
143+
isLoading = true
144+
errorMessage = nil
145+
146+
// 네트워크 요청 시뮬레이션 (2초)
147+
try? await Task.sleep(for: .seconds(2))
148+
149+
// 90% 확률로 성공
150+
if Double.random(in: 0...1) > 0.1 {
151+
items.removeAll()
152+
isCheckoutComplete = true
153+
} else {
154+
errorMessage = "결제 처리 중 오류가 발생했습니다. 다시 시도해주세요."
155+
}
156+
157+
isLoading = false
158+
}
159+
160+
/// 결제 완료 상태 초기화
161+
func resetCheckout() {
162+
isCheckoutComplete = false
163+
errorMessage = nil
164+
}
165+
}
166+
167+
// MARK: - Preview Support
168+
169+
extension CartStore {
170+
/// 미리보기용 샘플 데이터가 채워진 Store
171+
static var preview: CartStore {
172+
let store = CartStore()
173+
store.items = CartItem.samples
174+
return store
175+
}
176+
177+
/// 비어있는 Store
178+
static var empty: CartStore {
179+
CartStore()
180+
}
181+
}

0 commit comments

Comments
 (0)