feat(character-gallery): 이미지 구매 기능 추가
This commit is contained in:
		@@ -13,6 +13,7 @@ enum CharacterApi {
 | 
			
		||||
    case getCharacterDetail(characterId: Int)
 | 
			
		||||
    case getCharacterImageList(characterId: Int, page: Int, size: Int)
 | 
			
		||||
    case getMyCharacterImageList(characterId: Int64, page: Int, size: Int)
 | 
			
		||||
    case purchaseCharacterImage(imageId: Int)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension CharacterApi: TargetType {
 | 
			
		||||
@@ -31,10 +32,21 @@ extension CharacterApi: TargetType {
 | 
			
		||||
            
 | 
			
		||||
        case .getMyCharacterImageList:
 | 
			
		||||
            return "/api/chat/character/image/list"
 | 
			
		||||
            
 | 
			
		||||
        case .purchaseCharacterImage:
 | 
			
		||||
            return "/api/chat/character/image/purchase"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var method: Moya.Method { .get }
 | 
			
		||||
    var method: Moya.Method {
 | 
			
		||||
        switch self {
 | 
			
		||||
        case .purchaseCharacterImage:
 | 
			
		||||
            return .post
 | 
			
		||||
            
 | 
			
		||||
        default:
 | 
			
		||||
            return .get
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    var task: Moya.Task {
 | 
			
		||||
        switch self {
 | 
			
		||||
@@ -60,6 +72,9 @@ extension CharacterApi: TargetType {
 | 
			
		||||
                ],
 | 
			
		||||
                encoding: URLEncoding.queryString
 | 
			
		||||
            )
 | 
			
		||||
            
 | 
			
		||||
        case .purchaseCharacterImage(let imageId):
 | 
			
		||||
            return .requestJSONEncodable(CharacterImagePurchaseRequest(imageId: imageId))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 
 | 
			
		||||
@@ -40,4 +40,8 @@ final class CharacterDetailGalleryRepository {
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func purchaseCharacterImage(imageId: Int) -> AnyPublisher<Response, MoyaError> {
 | 
			
		||||
        return characterApi.requestPublisher(.purchaseCharacterImage(imageId: imageId))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,14 +73,65 @@ final class CharacterDetailGalleryViewModel: ObservableObject {
 | 
			
		||||
            isShowImageViewer = true
 | 
			
		||||
        } else {
 | 
			
		||||
            // 미소유 이미지 - 구매 다이얼로그 표시
 | 
			
		||||
            selectedImageIndex = index
 | 
			
		||||
            isShowPurchaseDialog = true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func onPurchaseConfirm() {
 | 
			
		||||
        // TODO: 구매 API 연동 구현 예정
 | 
			
		||||
        print("구매 확인: \(selectedItem?.id ?? 0)")
 | 
			
		||||
        isShowPurchaseDialog = false
 | 
			
		||||
        if isLoading {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if let imageItem = selectedItem {
 | 
			
		||||
            isLoading = true
 | 
			
		||||
            repository.purchaseCharacterImage(imageId: imageItem.id)
 | 
			
		||||
                .sink { result in
 | 
			
		||||
                    switch result {
 | 
			
		||||
                    case .finished:
 | 
			
		||||
                        DEBUG_LOG("finish")
 | 
			
		||||
                    case .failure(let error):
 | 
			
		||||
                        ERROR_LOG(error.localizedDescription)
 | 
			
		||||
                    }
 | 
			
		||||
                } receiveValue: { [unowned self] response in
 | 
			
		||||
                    let responseData = response.data
 | 
			
		||||
                    self.isLoading = false
 | 
			
		||||
                    
 | 
			
		||||
                    do {
 | 
			
		||||
                        let jsonDecoder = JSONDecoder()
 | 
			
		||||
                        let decoded = try jsonDecoder.decode(ApiResponse<CharacterImagePurchaseResponse>.self, from: responseData)
 | 
			
		||||
                        
 | 
			
		||||
                        if let data = decoded.data, decoded.success {
 | 
			
		||||
                            var copyGalleryItems = self.galleryItems
 | 
			
		||||
                            copyGalleryItems[self.selectedImageIndex].imageUrl = data.imageUrl
 | 
			
		||||
                            copyGalleryItems[self.selectedImageIndex].isOwned = true
 | 
			
		||||
                            self.galleryItems = copyGalleryItems
 | 
			
		||||
                            self.ownedCount += 1
 | 
			
		||||
                        } else {
 | 
			
		||||
                            if let message = decoded.message {
 | 
			
		||||
                                self.errorMessage = message
 | 
			
		||||
                            } else {
 | 
			
		||||
                                self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                            }
 | 
			
		||||
                            
 | 
			
		||||
                            self.isShowPopup = true
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        self.selectedImageIndex = 0
 | 
			
		||||
                        self.selectedItem = nil
 | 
			
		||||
                    } catch {
 | 
			
		||||
                        self.selectedImageIndex = 0
 | 
			
		||||
                        self.selectedItem = nil
 | 
			
		||||
                        ERROR_LOG(String(describing: error))
 | 
			
		||||
                        self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
			
		||||
                        self.isShowPopup = true
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .store(in: &subscription)
 | 
			
		||||
        } else {
 | 
			
		||||
            isShowPurchaseDialog = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    func onPurchaseCancel() {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ struct CharacterImageListResponse: Decodable {
 | 
			
		||||
 | 
			
		||||
struct CharacterImageListItemResponse: Decodable, Hashable {
 | 
			
		||||
    let id: Int
 | 
			
		||||
    let imageUrl: String
 | 
			
		||||
    let isOwned: Bool
 | 
			
		||||
    var imageUrl: String
 | 
			
		||||
    var isOwned: Bool
 | 
			
		||||
    let imagePriceCan: Int
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
//
 | 
			
		||||
//  CharacterImagePurchaseRequest.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 9/5/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
struct CharacterImagePurchaseRequest: Encodable {
 | 
			
		||||
    let imageId: Int
 | 
			
		||||
    let container: String = "ios"
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
//
 | 
			
		||||
//  CharacterImagePurchaseResponse.swift
 | 
			
		||||
//  SodaLive
 | 
			
		||||
//
 | 
			
		||||
//  Created by klaus on 9/5/25.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
struct CharacterImagePurchaseResponse: Decodable {
 | 
			
		||||
    let imageUrl: String
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user