라이브 성별 제한 옵션 추가

라이브 생성과 수정 요청에 성별 제한 값을 포함한다.
라이브 정보 조회 응답에 성별 제한 값을 제공한다.
This commit is contained in:
Yu Sung
2026-02-02 18:20:26 +09:00
parent b985af4497
commit 5159debf7f
9 changed files with 135 additions and 6 deletions

View File

@@ -672,6 +672,12 @@ enum I18n {
static var allAges: String { pick(ko: "전체 연령", en: "All ages", ja: "全年齢") } static var allAges: String { pick(ko: "전체 연령", en: "All ages", ja: "全年齢") }
static var over19: String { pick(ko: "19세 이상", en: "19+", ja: "R-18") } static var over19: String { pick(ko: "19세 이상", en: "19+", ja: "R-18") }
//
static var genderRestrictionTitle: String { pick(ko: "성별 제한", en: "Gender restriction", ja: "性別制限") }
static var genderAll: String { pick(ko: "전체", en: "All", ja: "全体") }
static var genderMaleOnly: String { pick(ko: "남자만", en: "Male only", ja: "男性のみ") }
static var genderFemaleOnly: String { pick(ko: "여자만", en: "Female only", ja: "女性のみ") }
// / // /
static var recentDataLoaded: String { pick(ko: "최근데이터를 불러왔습니다.", en: "Recent data has been loaded.", ja: "最新データを読み込みました。") } static var recentDataLoaded: String { pick(ko: "최근데이터를 불러왔습니다.", en: "Recent data has been loaded.", ja: "最新データを読み込みました。") }
static var recentDataLoadFailed: String { pick(ko: "최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요.", en: "Failed to load recent data.\ntry again.", ja: "最近のデータを読み込めませんでした。\n恐れ入りますが、もう一度お試しください。") } static var recentDataLoadFailed: String { pick(ko: "최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요.", en: "Failed to load recent data.\ntry again.", ja: "最近のデータを読み込めませんでした。\n恐れ入りますが、もう一度お試しください。") }

View File

@@ -14,6 +14,7 @@ struct CreateLiveRoomRequest: Encodable {
let tags: [String] let tags: [String]
let numberOfPeople: Int let numberOfPeople: Int
var isAdult: Bool = false var isAdult: Bool = false
var genderRestriction: LiveRoomCreateViewModel.GenderRestriction = .ALL
var price = 0 var price = 0
var type: LiveRoomCreateViewModel.LiveRoomType = .OPEN var type: LiveRoomCreateViewModel.LiveRoomType = .OPEN
var password: String? = nil var password: String? = nil

View File

@@ -13,4 +13,5 @@ struct GetRecentRoomInfoResponse: Decodable {
let coverImageUrl: String let coverImageUrl: String
let coverImagePath: String let coverImagePath: String
let numberOfPeople: Int let numberOfPeople: Int
let genderRestriction: LiveRoomCreateViewModel.GenderRestriction
} }

View File

@@ -175,6 +175,10 @@ struct LiveRoomCreateView: View {
AdultSettingView() AdultSettingView()
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
.padding(.top, 33.3) .padding(.top, 33.3)
GenderRestrictionView()
.frame(width: screenSize().width - 26.7)
.padding(.top, 33.3)
} }
PriceSettingView() PriceSettingView()
@@ -698,6 +702,56 @@ struct LiveRoomCreateView: View {
} }
} }
} }
@ViewBuilder
func GenderRestrictionView() -> some View {
VStack(spacing: 13.3) {
Text(I18n.CreateLive.genderRestrictionTitle)
.appFont(size: 16.7, weight: .bold)
.foregroundColor(Color.grayee)
.frame(width: screenSize().width - 26.7, alignment: .leading)
HStack(spacing: 13.3) {
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderAll,
restriction: .ALL,
buttonWidth: (screenSize().width - 53) / 3
)
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderMaleOnly,
restriction: .MALE_ONLY,
buttonWidth: (screenSize().width - 53) / 3
)
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderFemaleOnly,
restriction: .FEMALE_ONLY,
buttonWidth: (screenSize().width - 53) / 3
)
}
}
}
@ViewBuilder
func GenderRestrictionSelectButton(
title: String,
restriction: LiveRoomCreateViewModel.GenderRestriction,
buttonWidth: CGFloat
) -> some View {
SelectedButtonView(
title: title,
isActive: true,
isSelected: viewModel.genderRestriction == restriction
)
.frame(width: buttonWidth)
.onTapGesture {
hideKeyboard()
if viewModel.genderRestriction != restriction {
viewModel.genderRestriction = restriction
}
}
}
@ViewBuilder @ViewBuilder
func PriceSettingView() -> some View { func PriceSettingView() -> some View {

View File

@@ -23,6 +23,10 @@ final class LiveRoomCreateViewModel: ObservableObject {
enum LiveRoomType: String, Codable { enum LiveRoomType: String, Codable {
case OPEN, PRIVATE case OPEN, PRIVATE
} }
enum GenderRestriction: String, Codable {
case ALL, MALE_ONLY, FEMALE_ONLY
}
let prices = [0, 100, 300, 500, 1000, 2000] let prices = [0, 100, 300, 500, 1000, 2000]
@@ -61,6 +65,7 @@ final class LiveRoomCreateViewModel: ObservableObject {
} }
@Published var isAdult = false @Published var isAdult = false
@Published var genderRestriction: GenderRestriction = .ALL
@Published var priceString = "0" { @Published var priceString = "0" {
didSet { didSet {
if priceString.count > 5 { if priceString.count > 5 {
@@ -134,6 +139,7 @@ final class LiveRoomCreateViewModel: ObservableObject {
self.coverImageUrl = data.coverImageUrl self.coverImageUrl = data.coverImageUrl
self.coverImagePath = data.coverImagePath self.coverImagePath = data.coverImagePath
self.numberOfPeople = String(data.numberOfPeople) self.numberOfPeople = String(data.numberOfPeople)
self.genderRestriction = data.genderRestriction
self.errorMessage = I18n.CreateLive.recentDataLoaded self.errorMessage = I18n.CreateLive.recentDataLoaded
self.isShowPopup = true self.isShowPopup = true
@@ -173,6 +179,7 @@ final class LiveRoomCreateViewModel: ObservableObject {
tags: tags, tags: tags,
numberOfPeople: Int(numberOfPeople)!, numberOfPeople: Int(numberOfPeople)!,
isAdult: isAdult, isAdult: isAdult,
genderRestriction: genderRestriction,
price: price, price: price,
type: roomType, type: roomType,
password: (roomType == .PRIVATE && !password.trimmingCharacters(in: .whitespaces).isEmpty) ? password : nil, password: (roomType == .PRIVATE && !password.trimmingCharacters(in: .whitespaces).isEmpty) ? password : nil,

View File

@@ -14,6 +14,7 @@ struct GetRoomDetailResponse: Decodable {
let notice: String let notice: String
let isPaid: Bool let isPaid: Bool
let isAdult: Bool let isAdult: Bool
let genderRestriction: LiveRoomCreateViewModel.GenderRestriction?
let isPrivateRoom: Bool let isPrivateRoom: Bool
let password: String? let password: String?
let tags: [String] let tags: [String]

View File

@@ -17,4 +17,5 @@ struct EditLiveRoomInfoRequest: Encodable {
var menuPan: String = "" var menuPan: String = ""
var isActiveMenuPan: Bool? = nil var isActiveMenuPan: Bool? = nil
var isAdult: Bool? = nil var isAdult: Bool? = nil
var genderRestriction: LiveRoomCreateViewModel.GenderRestriction? = nil
} }

View File

@@ -45,13 +45,17 @@ struct LiveRoomEditView: View {
NumberOfPeopleLimitView() NumberOfPeopleLimitView()
.frame(width: screenSize().width - 26.7) .frame(width: screenSize().width - 26.7)
.padding(.top, 33.3) .padding(.top, 33.3)
GenderRestrictionView()
.frame(width: screenSize().width - 26.7)
.padding(.top, 33.3)
if !viewModel.isLoading { if !viewModel.isLoading {
Text("라이브 수정") Text("라이브 수정")
.appFont(size: 18.3, weight: .bold) .appFont(size: 18.3, weight: .bold)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.frame(width: screenSize().width - 26.7, height: 50) .frame(width: screenSize().width - 26.7, height: 50)
.background(Color(hex: "9970ff")) .background(Color.button)
.cornerRadius(10) .cornerRadius(10)
.padding(.top, 30) .padding(.top, 30)
.onTapGesture { .onTapGesture {
@@ -87,7 +91,7 @@ struct LiveRoomEditView: View {
.padding(.vertical, 13.3) .padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center) .frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium) .appFont(size: 12, weight: .medium)
.background(Color(hex: "9970ff")) .background(Color.button)
.foregroundColor(Color.white) .foregroundColor(Color.white)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.cornerRadius(20) .cornerRadius(20)
@@ -175,7 +179,7 @@ struct LiveRoomEditView: View {
.frame(width: buttonWidth, height: 48.7) .frame(width: buttonWidth, height: 48.7)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 6.7) RoundedRectangle(cornerRadius: 6.7)
.stroke(Color(hex: "9970ff"), lineWidth: 1.3) .stroke(Color.button, lineWidth: 1.3)
) )
} }
} }
@@ -195,7 +199,7 @@ struct LiveRoomEditView: View {
.frame(width: buttonWidth, height: 48.7) .frame(width: buttonWidth, height: 48.7)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 6.7) RoundedRectangle(cornerRadius: 6.7)
.stroke(Color(hex: "9970ff"), lineWidth: 1.3) .stroke(Color.button, lineWidth: 1.3)
) )
} }
} }
@@ -227,6 +231,56 @@ struct LiveRoomEditView: View {
.cornerRadius(6.7) .cornerRadius(6.7)
} }
} }
@ViewBuilder
func GenderRestrictionView() -> some View {
VStack(spacing: 13.3) {
Text(I18n.CreateLive.genderRestrictionTitle)
.appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "eeeeee"))
.frame(width: screenSize().width - 26.7, alignment: .leading)
HStack(spacing: 13.3) {
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderAll,
restriction: .ALL,
buttonWidth: (screenSize().width - 53) / 3
)
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderMaleOnly,
restriction: .MALE_ONLY,
buttonWidth: (screenSize().width - 53) / 3
)
GenderRestrictionSelectButton(
title: I18n.CreateLive.genderFemaleOnly,
restriction: .FEMALE_ONLY,
buttonWidth: (screenSize().width - 53) / 3
)
}
}
}
@ViewBuilder
func GenderRestrictionSelectButton(
title: String,
restriction: LiveRoomCreateViewModel.GenderRestriction,
buttonWidth: CGFloat
) -> some View {
SelectedButtonView(
title: title,
isActive: true,
isSelected: viewModel.genderRestriction == restriction
)
.frame(width: buttonWidth)
.onTapGesture {
hideKeyboard()
if viewModel.genderRestriction != restriction {
viewModel.genderRestriction = restriction
}
}
}
@ViewBuilder @ViewBuilder
func SelectDateView() -> some View { func SelectDateView() -> some View {

View File

@@ -24,6 +24,7 @@ final class LiveRoomEditViewModel: ObservableObject {
@Published var numberOfPeople = "" @Published var numberOfPeople = ""
@Published var reservationDateString: String = "" @Published var reservationDateString: String = ""
@Published var reservationTimeString: String = "" @Published var reservationTimeString: String = ""
@Published var genderRestriction: LiveRoomCreateViewModel.GenderRestriction = .ALL
@Published var errorMessage = "" @Published var errorMessage = ""
@Published var isShowPopup = false @Published var isShowPopup = false
@@ -51,6 +52,7 @@ final class LiveRoomEditViewModel: ObservableObject {
title = room!.title title = room!.title
notice = room!.notice notice = room!.notice
numberOfPeople = String(room!.numberOfParticipantsTotal) numberOfPeople = String(room!.numberOfParticipantsTotal)
genderRestriction = room!.genderRestriction ?? .ALL
if let beginDate = room!.beginDateTimeUtc.parseUtcIsoDate() { if let beginDate = room!.beginDateTimeUtc.parseUtcIsoDate() {
reservationDate = beginDate reservationDate = beginDate
@@ -80,14 +82,16 @@ final class LiveRoomEditViewModel: ObservableObject {
notice: room.notice != notice ? notice : nil, notice: room.notice != notice ? notice : nil,
numberOfPeople: room.numberOfParticipantsTotal != Int(numberOfPeople)! ? Int(numberOfPeople)! : nil, numberOfPeople: room.numberOfParticipantsTotal != Int(numberOfPeople)! ? Int(numberOfPeople)! : nil,
beginDateTimeString: beginDateTimeStr != beginDateTime ? beginDateTime : nil, beginDateTimeString: beginDateTimeStr != beginDateTime ? beginDateTime : nil,
timezone: TimeZone.current.identifier timezone: TimeZone.current.identifier,
genderRestriction: (room.genderRestriction ?? .ALL) != genderRestriction ? genderRestriction : nil
) )
if ( if (
request.title == nil && request.title == nil &&
request.notice == nil && request.notice == nil &&
request.numberOfPeople == nil && request.numberOfPeople == nil &&
request.beginDateTimeString == nil request.beginDateTimeString == nil &&
request.genderRestriction == nil
) { ) {
self.errorMessage = "변경사항이 없습니다." self.errorMessage = "변경사항이 없습니다."
self.isShowPopup = true self.isShowPopup = true