커뮤니티 유료 게시글 조회, 구매 기능 추가

This commit is contained in:
Yu Sung 2024-05-24 16:19:43 +09:00
parent 0a96509b35
commit 3ae5ea776c
13 changed files with 337 additions and 63 deletions

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_lock_bb.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,92 @@
//
// CommunityPostPurchaseDialog.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CommunityPostPurchaseDialog: View {
@Binding var isShowing: Bool
let can: Int
let confirmAction: () -> Void
var body: some View {
GeometryReader { geo in
ZStack {
Color.black
.opacity(0.5)
.frame(width: geo.size.width, height: geo.size.height)
VStack(spacing: 0) {
Text("게시글 보기")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.graybb)
.padding(.top, 40)
Text("게시글을\n확인하시겠습니까?")
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color.graybb)
.multilineTextAlignment(.center)
.padding(.top, 12)
.padding(.horizontal, 13.3)
HStack(spacing: 13.3) {
Text("취소")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.button)
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) / 3)
.background(Color.bg)
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture {
isShowing = false
}
HStack(spacing: 0) {
Text("\(can)")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.white)
Image("ic_can")
.resizable()
.frame(width: 20, height: 20)
Text("으로 보기")
.font(.custom(Font.bold.rawValue, size: 15.3))
.foregroundColor(Color.white)
}
.padding(.vertical, 16)
.frame(width: (geo.size.width - 66.7) * 2 / 3)
.background(Color.button)
.cornerRadius(8)
.onTapGesture {
confirmAction()
isShowing = false
}
}
.padding(.top, 26.7)
.padding(.bottom, 16.7)
}
.frame(width: geo.size.width - 26.7, alignment: .center)
.background(Color.gray22)
.cornerRadius(10)
}
}
}
}
#Preview {
CommunityPostPurchaseDialog(
isShowing: .constant(true),
can: 10,
confirmAction: {}
)
}

View File

@ -0,0 +1,39 @@
//
// CreatorCommunityAllItemLockView.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import SwiftUI
struct CreatorCommunityAllItemLockView: View {
let price: Int
let onClickPurchaseContent: () -> Void
var body: some View {
VStack(spacing: 26.7) {
Image("ic_lock_bb")
Text("\(price)캔으로 게시글 보기")
.font(.custom(Font.bold.rawValue, size: 12))
.foregroundColor(Color.button)
.padding(.horizontal, 21)
.padding(.vertical, 11)
.overlay(
RoundedRectangle(cornerRadius: 26.7)
.stroke(Color.button, lineWidth: 1)
)
.onTapGesture { onClickPurchaseContent() }
}
.frame(width: screenSize().width - 42, height: screenSize().width - 42)
.background(Color.gray33)
.cornerRadius(5.3)
}
}
#Preview {
CreatorCommunityAllItemLockView(price: 100) {
}
}

View File

@ -15,6 +15,7 @@ struct CreatorCommunityAllItemView: View {
let onClickComment: () -> Void let onClickComment: () -> Void
let onClickWriteComment: (String) -> Void let onClickWriteComment: (String) -> Void
let onClickShowReportMenu: () -> Void let onClickShowReportMenu: () -> Void
let onClickPurchaseContent: () -> Void
@State var isLike = false @State var isLike = false
@State var likeCount = 0 @State var likeCount = 0
@ -25,13 +26,15 @@ struct CreatorCommunityAllItemView: View {
onClickLike: @escaping () -> Void, onClickLike: @escaping () -> Void,
onClickComment: @escaping () -> Void, onClickComment: @escaping () -> Void,
onClickWriteComment: @escaping (String) -> Void, onClickWriteComment: @escaping (String) -> Void,
onClickShowReportMenu: @escaping () -> Void onClickShowReportMenu: @escaping () -> Void,
onClickPurchaseContent: @escaping () -> Void
) { ) {
self.item = item self.item = item
self.onClickLike = onClickLike self.onClickLike = onClickLike
self.onClickComment = onClickComment self.onClickComment = onClickComment
self.onClickWriteComment = onClickWriteComment self.onClickWriteComment = onClickWriteComment
self.onClickShowReportMenu = onClickShowReportMenu self.onClickShowReportMenu = onClickShowReportMenu
self.onClickPurchaseContent = onClickPurchaseContent
self._isLike = State(initialValue: item.isLike) self._isLike = State(initialValue: item.isLike)
self._likeCount = State(initialValue: item.likeCount) self._likeCount = State(initialValue: item.likeCount)
@ -58,66 +61,74 @@ struct CreatorCommunityAllItemView: View {
Spacer() Spacer()
Image("ic_seemore_vertical") if item.price <= 0 || item.existOrdered {
.padding(.trailing, 8.3) Image("ic_seemore_vertical")
.onTapGesture { onClickShowReportMenu() } .padding(.trailing, 8.3)
.onTapGesture { onClickShowReportMenu() }
}
} }
DetectableTextView(text: item.content, textSize: 13.3, font: Font.medium.rawValue) if item.price <= 0 || item.existOrdered {
.frame( DetectableTextView(text: item.content, textSize: 13.3, font: Font.medium.rawValue)
width: screenSize().width - 16, .frame(
height: textHeight width: screenSize().width - 16,
) height: textHeight
.onAppear {
self.textHeight = self.estimatedHeight(
for: item.content,
width: screenSize().width - 16
) )
} .onAppear {
.onChange(of: item.content) { newText in self.textHeight = self.estimatedHeight(
self.textHeight = self.estimatedHeight( for: item.content,
for: newText, width: screenSize().width - 16
width: screenSize().width - 16 )
)
}
if let imageUrl = item.imageUrl {
KFImage(URL(string: imageUrl))
.resizable()
.frame(maxWidth: .infinity)
.scaledToFit()
}
HStack(spacing: 8) {
IconAndTitleToggleButton(
isChecked: isLike,
title: "\(likeCount)",
normalIconName: "ic_audio_content_heart_normal",
checkedIconName: "ic_audio_content_heart_pressed"
) {
if isLike {
isLike = false
likeCount -= 1
} else {
isLike = true
likeCount += 1
} }
onClickLike() .onChange(of: item.content) { newText in
self.textHeight = self.estimatedHeight(
for: newText,
width: screenSize().width - 16
)
}
if let imageUrl = item.imageUrl {
KFImage(URL(string: imageUrl))
.resizable()
.frame(maxWidth: .infinity)
.scaledToFit()
} }
}
.frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 8) {
IconAndTitleToggleButton(
if item.isCommentAvailable { isChecked: isLike,
CreatorCommunityCommentView( title: "\(likeCount)",
commentCount: item.commentCount, normalIconName: "ic_audio_content_heart_normal",
commentItem: item.firstComment, checkedIconName: "ic_audio_content_heart_pressed"
onClickWriteComment: onClickWriteComment ) {
) if isLike {
.onTapGesture { isLike = false
if item.commentCount > 0 { likeCount -= 1
onClickComment() } else {
isLike = true
likeCount += 1
}
onClickLike()
} }
} }
.frame(maxWidth: .infinity, alignment: .leading)
if item.isCommentAvailable {
CreatorCommunityCommentView(
commentCount: item.commentCount,
commentItem: item.firstComment,
onClickWriteComment: onClickWriteComment
)
.onTapGesture {
if item.commentCount > 0 {
onClickComment()
}
}
}
} else {
CreatorCommunityAllItemLockView(
price: item.price,
onClickPurchaseContent: onClickPurchaseContent)
} }
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
@ -144,10 +155,12 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,
existOrdered: false,
likeCount: 10, likeCount: 10,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil
@ -155,7 +168,8 @@ struct CreatorCommunityAllItemView_Previews: PreviewProvider {
onClickLike: {}, onClickLike: {},
onClickComment: {}, onClickComment: {},
onClickWriteComment: { _ in }, onClickWriteComment: { _ in },
onClickShowReportMenu: {} onClickShowReportMenu: {},
onClickPurchaseContent: {}
) )
} }
} }

View File

@ -41,6 +41,12 @@ struct CreatorCommunityAllView: View {
onClickShowReportMenu: { onClickShowReportMenu: {
viewModel.postId = item.postId viewModel.postId = item.postId
viewModel.isShowReportMenu = true viewModel.isShowReportMenu = true
},
onClickPurchaseContent: {
viewModel.postId = item.postId
viewModel.postPrice = item.price
viewModel.postIndex = index
viewModel.isShowPostPurchaseView = true
} }
) )
.onAppear { .onAppear {
@ -121,6 +127,15 @@ struct CreatorCommunityAllView: View {
} }
) )
} }
if viewModel.isShowPostPurchaseView {
CommunityPostPurchaseDialog(
isShowing: $viewModel.isShowPostPurchaseView,
can: viewModel.postPrice
) {
viewModel.purchaseCommunityPost()
}
}
} }
} }
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {

View File

@ -21,6 +21,8 @@ class CreatorCommunityAllViewModel: ObservableObject {
@Published private(set) var communityPostList = [GetCommunityPostListResponse]() @Published private(set) var communityPostList = [GetCommunityPostListResponse]()
@Published var postId = 0 @Published var postId = 0
@Published var postPrice = 0
@Published var postIndex = -1
@Published var isShowCommentListView = false { @Published var isShowCommentListView = false {
didSet { didSet {
@ -46,6 +48,16 @@ class CreatorCommunityAllViewModel: ObservableObject {
} }
} }
@Published var isShowPostPurchaseView = false {
didSet {
if !isShowPostPurchaseView {
postId = 0
postPrice = 0
postIndex = -1
}
}
}
var creatorId = 0 var creatorId = 0
var page = 1 var page = 1
@ -254,4 +266,51 @@ class CreatorCommunityAllViewModel: ObservableObject {
self.isLoading = false self.isLoading = false
} }
} }
func purchaseCommunityPost() {
let postId = postId
let postIndex = postIndex
if !isLoading {
isLoading = true
repository
.purchaseCommunityPost(postId: postId)
.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
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetCommunityPostListResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
if postIndex >= 0 {
communityPostList[postIndex] = data
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
self.isLoading = false
}
.store(in: &subscription)
}
}
} }

View File

@ -0,0 +1,14 @@
//
// PurchasePostRequest.swift
// SodaLive
//
// Created by klaus on 5/24/24.
//
import Foundation
struct PurchasePostRequest: Encodable {
let postId: Int
let container: String = "ios"
let timezone: String = TimeZone.current.identifier
}

View File

@ -19,6 +19,7 @@ enum CreatorCommunityApi {
case getCommentReplyList(commentId: Int, page: Int, size: Int) case getCommentReplyList(commentId: Int, page: Int, size: Int)
case modifyComment(request: ModifyCommunityPostCommentRequest) case modifyComment(request: ModifyCommunityPostCommentRequest)
case getLatestPostListFromCreatorsYouFollow case getLatestPostListFromCreatorsYouFollow
case purchaseCommunityPost(postId: Int)
} }
extension CreatorCommunityApi: TargetType { extension CreatorCommunityApi: TargetType {
@ -48,12 +49,15 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow: case .getLatestPostListFromCreatorsYouFollow:
return "/creator-community/latest" return "/creator-community/latest"
case .purchaseCommunityPost:
return "/creator-community/purchase"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .createCommunityPost, .communityPostLike, .createCommunityPostComment: case .createCommunityPost, .communityPostLike, .createCommunityPostComment, .purchaseCommunityPost:
return .post return .post
case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail, .getLatestPostListFromCreatorsYouFollow: case .getCommunityPostList, .getCommunityPostCommentList, .getCommentReplyList, .getCommunityPostDetail, .getLatestPostListFromCreatorsYouFollow:
@ -115,6 +119,9 @@ extension CreatorCommunityApi: TargetType {
case .getLatestPostListFromCreatorsYouFollow: case .getLatestPostListFromCreatorsYouFollow:
let parameters = ["timezone": TimeZone.current.identifier] as [String: Any] let parameters = ["timezone": TimeZone.current.identifier] as [String: Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .purchaseCommunityPost(let postId):
return .requestJSONEncodable(PurchasePostRequest(postId: postId))
} }
} }

View File

@ -22,19 +22,19 @@ struct CreatorCommunityItemView: View {
Text(item.creatorNickname) Text(item.creatorNickname)
.font(.custom(Font.medium.rawValue, size: 13.3)) .font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee")) .foregroundColor(Color.grayee)
Spacer() Spacer()
Text(item.date) Text(item.date)
.font(.custom(Font.light.rawValue, size: 13.3)) .font(.custom(Font.light.rawValue, size: 13.3))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
HStack(spacing: 0) { HStack(spacing: 0) {
Text(item.content) Text(item.content)
.font(.custom(Font.medium.rawValue, size: 12)) .font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color(hex: "bbbbbb")) .foregroundColor(Color.graybb)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.lineLimit(3) .lineLimit(3)
@ -45,9 +45,10 @@ struct CreatorCommunityItemView: View {
.resizable() .resizable()
.frame(width: 53.3, height: 53.3) .frame(width: 53.3, height: 53.3)
.cornerRadius(4.7) .cornerRadius(4.7)
.blur(radius: item.existOrdered ? 0 : 15)
} else { } else {
Rectangle() Rectangle()
.foregroundColor(Color(hex: "222222").opacity(0)) .foregroundColor(Color.gray22.opacity(0))
.frame(width: 53.3, height: 53.3) .frame(width: 53.3, height: 53.3)
} }
} }
@ -60,7 +61,7 @@ struct CreatorCommunityItemView: View {
Text("\(item.likeCount)") Text("\(item.likeCount)")
.font(.custom(Font.medium.rawValue, size: 11)) .font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
HStack(spacing: 6) { HStack(spacing: 6) {
@ -70,13 +71,13 @@ struct CreatorCommunityItemView: View {
Text("\(item.commentCount)") Text("\(item.commentCount)")
.font(.custom(Font.medium.rawValue, size: 11)) .font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color(hex: "777777")) .foregroundColor(Color.gray77)
} }
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(13.3) .padding(13.3)
.background(Color(hex: "222222")) .background(Color.gray22)
.cornerRadius(11) .cornerRadius(11)
} }
} }
@ -91,10 +92,12 @@ struct CreatorCommunityItemView_Previews: PreviewProvider {
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "안녕하세요", content: "안녕하세요",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: false, isLike: false,
existOrdered: false,
likeCount: 10, likeCount: 10,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil

View File

@ -52,4 +52,8 @@ class CreatorCommunityRepository {
func getLatestPostListFromCreatorsYouFollow() -> AnyPublisher<Response, MoyaError> { func getLatestPostListFromCreatorsYouFollow() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getLatestPostListFromCreatorsYouFollow) return api.requestPublisher(.getLatestPostListFromCreatorsYouFollow)
} }
func purchaseCommunityPost(postId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.purchaseCommunityPost(postId: postId))
}
} }

View File

@ -12,10 +12,12 @@ struct GetCommunityPostListResponse: Decodable {
let creatorProfileUrl: String let creatorProfileUrl: String
let imageUrl: String? let imageUrl: String?
let content: String let content: String
let price: Int
let date: String let date: String
let isCommentAvailable: Bool let isCommentAvailable: Bool
let isAdult: Bool let isAdult: Bool
let isLike: Bool let isLike: Bool
let existOrdered: Bool
let likeCount: Int let likeCount: Int
let commentCount: Int let commentCount: Int
let firstComment: GetCommunityPostCommentListItem? let firstComment: GetCommunityPostCommentListItem?

View File

@ -38,10 +38,12 @@ struct SectionCommunityPostView_Previews: PreviewProvider {
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,
existOrdered: false,
likeCount: 10, likeCount: 10,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil
@ -53,10 +55,12 @@ struct SectionCommunityPostView_Previews: PreviewProvider {
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png", creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!", content: "너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!너무 조하유 앞으로도 좋은 라이브 많이 들려주세요!",
price: 10,
date: "3일전", date: "3일전",
isCommentAvailable: false, isCommentAvailable: false,
isAdult: false, isAdult: false,
isLike: true, isLike: true,
existOrdered: false,
likeCount: 20, likeCount: 20,
commentCount: 0, commentCount: 0,
firstComment: nil firstComment: nil