feat(gift): 선물 발송 표시를 보정한다
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_send_gift.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 815 B |
@@ -197,7 +197,7 @@ enum AppStep {
|
|||||||
|
|
||||||
case seriesMain
|
case seriesMain
|
||||||
|
|
||||||
case giftSend(recipientMemberId: Int, recipientCreatorNickname: String)
|
case giftSend(recipientMemberId: Int, recipientCreatorNickname: String, recipientCreatorProfileImageUrl: String)
|
||||||
|
|
||||||
case giftBoxList
|
case giftBoxList
|
||||||
|
|
||||||
|
|||||||
@@ -374,8 +374,8 @@ struct AppStepLayerView: View {
|
|||||||
case .seriesMain:
|
case .seriesMain:
|
||||||
SeriesMainView()
|
SeriesMainView()
|
||||||
|
|
||||||
case .giftSend(let recipientMemberId, let recipientCreatorNickname):
|
case .giftSend(let recipientMemberId, let recipientCreatorNickname, let recipientCreatorProfileImageUrl):
|
||||||
GiftSendView(recipientMemberId: recipientMemberId, recipientCreatorNickname: recipientCreatorNickname)
|
GiftSendView(recipientMemberId: recipientMemberId, recipientCreatorNickname: recipientCreatorNickname, recipientCreatorProfileImageUrl: recipientCreatorProfileImageUrl)
|
||||||
|
|
||||||
case .giftBoxList:
|
case .giftBoxList:
|
||||||
GiftBoxListView()
|
GiftBoxListView()
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ struct CreatorChannelHeaderSection: View {
|
|||||||
CreatorChannelTalkActionSection(
|
CreatorChannelTalkActionSection(
|
||||||
recipientMemberId: creator.creatorId,
|
recipientMemberId: creator.creatorId,
|
||||||
recipientCreatorNickname: creator.nickname,
|
recipientCreatorNickname: creator.nickname,
|
||||||
|
recipientCreatorProfileImageUrl: creator.profileImageUrl,
|
||||||
isAiChatAvailable: creator.isAiChatAvailable,
|
isAiChatAvailable: creator.isAiChatAvailable,
|
||||||
isDmAvailable: creator.isDmAvailable,
|
isDmAvailable: creator.isDmAvailable,
|
||||||
isOwnCreatorChannel: isOwnCreatorChannel,
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
|
|||||||
+15
-6
@@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
struct CreatorChannelTalkActionSection: View {
|
struct CreatorChannelTalkActionSection: View {
|
||||||
let recipientMemberId: Int
|
let recipientMemberId: Int
|
||||||
let recipientCreatorNickname: String
|
let recipientCreatorNickname: String
|
||||||
|
let recipientCreatorProfileImageUrl: String
|
||||||
let isAiChatAvailable: Bool
|
let isAiChatAvailable: Bool
|
||||||
let isDmAvailable: Bool
|
let isDmAvailable: Bool
|
||||||
let isOwnCreatorChannel: Bool
|
let isOwnCreatorChannel: Bool
|
||||||
@@ -66,16 +67,24 @@ struct CreatorChannelTalkActionSection: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
AppState.shared.setAppStep(step: .giftSend(
|
AppState.shared.setAppStep(step: .giftSend(
|
||||||
recipientMemberId: recipientMemberId,
|
recipientMemberId: recipientMemberId,
|
||||||
recipientCreatorNickname: recipientCreatorNickname
|
recipientCreatorNickname: recipientCreatorNickname,
|
||||||
|
recipientCreatorProfileImageUrl: recipientCreatorProfileImageUrl
|
||||||
))
|
))
|
||||||
}) {
|
}) {
|
||||||
Text(I18n.CreatorChannelHome.sendGift)
|
HStack(spacing: SodaSpacing.s6) {
|
||||||
.appFont(size: 16, weight: .medium)
|
Image("ic_send_gift")
|
||||||
.foregroundColor(.white)
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
|
Text(I18n.CreatorChannelHome.sendGift)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(.black)
|
||||||
|
}
|
||||||
.padding(SodaSpacing.s12)
|
.padding(SodaSpacing.s12)
|
||||||
.overlay(
|
.background(
|
||||||
Capsule()
|
Capsule()
|
||||||
.stroke(Color.white.opacity(0.3), lineWidth: 1)
|
.fill(Color.white)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
struct GiftAgreementRow: View {
|
struct GiftAgreementRow: View {
|
||||||
let title: String
|
let title: String
|
||||||
@Binding var isAgreed: Bool
|
@Binding var isAgreed: Bool
|
||||||
|
var policyURL: URL? = GiftConstants.termsURL
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: SodaSpacing.s4) {
|
HStack(alignment: .top, spacing: SodaSpacing.s4) {
|
||||||
@@ -25,7 +26,7 @@ struct GiftAgreementRow: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.accessibilityValue(isAgreed ? "동의함" : "동의 안 함")
|
.accessibilityValue(isAgreed ? "동의함" : "동의 안 함")
|
||||||
|
|
||||||
if let url = GiftConstants.termsURL {
|
if let url = policyURL {
|
||||||
Link(destination: url) {
|
Link(destination: url) {
|
||||||
Text("(필수)")
|
Text("(필수)")
|
||||||
.underline()
|
.underline()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Foundation
|
|||||||
|
|
||||||
enum GiftConstants {
|
enum GiftConstants {
|
||||||
static let termsURL = URL(string: "https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3")
|
static let termsURL = URL(string: "https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3")
|
||||||
|
static let damagePolicyURL = URL(string: "https://app.notion.com/p/3f227b3596738018ba5dea2e5205293b")
|
||||||
static let addressSearchAvailable = false
|
static let addressSearchAvailable = false
|
||||||
static let inquiryStep: AppStep = .serviceCenter
|
static let inquiryStep: AppStep = .serviceCenter
|
||||||
static let feedbackStep: AppStep = .serviceCenter
|
static let feedbackStep: AppStep = .serviceCenter
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct GiftSendView: View {
|
struct GiftSendView: View {
|
||||||
|
private let recipientCreatorProfileImageUrl: String
|
||||||
@StateObject private var viewModel: GiftSendViewModel
|
@StateObject private var viewModel: GiftSendViewModel
|
||||||
@State private var hasAppeared = false
|
@State private var hasAppeared = false
|
||||||
|
|
||||||
init(recipientMemberId: Int, recipientCreatorNickname: String) {
|
init(recipientMemberId: Int, recipientCreatorNickname: String, recipientCreatorProfileImageUrl: String) {
|
||||||
|
self.recipientCreatorProfileImageUrl = recipientCreatorProfileImageUrl
|
||||||
_viewModel = StateObject(wrappedValue: GiftSendViewModel(
|
_viewModel = StateObject(wrappedValue: GiftSendViewModel(
|
||||||
recipientMemberId: recipientMemberId,
|
recipientMemberId: recipientMemberId,
|
||||||
recipientCreatorNickname: recipientCreatorNickname,
|
recipientCreatorNickname: recipientCreatorNickname,
|
||||||
@@ -12,7 +14,7 @@ struct GiftSendView: View {
|
|||||||
AppState.shared.replaceCurrentAppStep(
|
AppState.shared.replaceCurrentAppStep(
|
||||||
step: .giftDetail(applicationNo: applicationNo),
|
step: .giftDetail(applicationNo: applicationNo),
|
||||||
ifCurrentStep: { step in
|
ifCurrentStep: { step in
|
||||||
guard case .giftSend(let memberId, let nickname) = step else { return false }
|
guard case .giftSend(let memberId, let nickname, _) = step else { return false }
|
||||||
return memberId == recipientMemberId && nickname == recipientCreatorNickname
|
return memberId == recipientMemberId && nickname == recipientCreatorNickname
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -77,7 +79,8 @@ struct GiftSendView: View {
|
|||||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||||
sectionTitle("받는 크리에이터")
|
sectionTitle("받는 크리에이터")
|
||||||
HStack(spacing: SodaSpacing.s12) {
|
HStack(spacing: SodaSpacing.s12) {
|
||||||
Circle().fill(Color.gray800).frame(width: 38, height: 38)
|
DownsampledKFImage(url: URL(string: recipientCreatorProfileImageUrl), size: CGSize(width: 38, height: 38))
|
||||||
|
.clipShape(Circle())
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
Text(viewModel.recipientCreatorNickname)
|
Text(viewModel.recipientCreatorNickname)
|
||||||
.appFont(size: 16, weight: .bold)
|
.appFont(size: 16, weight: .bold)
|
||||||
@@ -155,7 +158,8 @@ struct GiftSendView: View {
|
|||||||
}
|
}
|
||||||
.disabled(viewModel.categories.isEmpty)
|
.disabled(viewModel.categories.isEmpty)
|
||||||
if viewModel.selectedCategory?.requiresDamageWaiver == true {
|
if viewModel.selectedCategory?.requiresDamageWaiver == true {
|
||||||
GiftAgreementRow(title: "파손 및 분실 면책 사항에 동의합니다.", isAgreed: $viewModel.damageWaiverAgreed)
|
GiftAgreementRow(title: "파손 및 분실 면책 사항에 동의합니다.", isAgreed: $viewModel.damageWaiverAgreed,
|
||||||
|
policyURL: GiftConstants.damagePolicyURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
ruby - "$root" "${1:-all}" <<'RUBY'
|
||||||
|
root, mode = ARGV
|
||||||
|
abort 'Usage: check-gift-phase11.sh [profile|policy|button]' unless %w[all profile policy button].include?(mode)
|
||||||
|
source_root = File.join(root, 'SodaLive/Sources')
|
||||||
|
read = lambda do |path|
|
||||||
|
File.read(File.join(source_root, path))
|
||||||
|
.gsub(%r{"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/}m) { |token| token.start_with?('"') ? token : '' }
|
||||||
|
.gsub(/\s+/, ' ').gsub(/\s*([(),:])\s*/, '\1').strip
|
||||||
|
end
|
||||||
|
check = lambda do |condition, message|
|
||||||
|
abort "RED gift-phase11 #{mode}: #{message}" unless condition
|
||||||
|
end
|
||||||
|
send_view = read.call('V2/Gift/Send/GiftSendView.swift')
|
||||||
|
|
||||||
|
if %w[all profile].include?(mode)
|
||||||
|
mode = 'profile'
|
||||||
|
step = read.call('App/AppStep.swift')
|
||||||
|
header = read.call('V2/CreatorChannel/Components/CreatorChannelHeaderSection.swift')
|
||||||
|
action = read.call('V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift')
|
||||||
|
content = read.call('ContentView.swift')
|
||||||
|
check.call(step.include?('case giftSend(recipientMemberId:Int,recipientCreatorNickname:String,recipientCreatorProfileImageUrl:String)'), 'AppStep.giftSend must carry recipientCreatorProfileImageUrl')
|
||||||
|
check.call(header.match?(/CreatorChannelTalkActionSection\(recipientMemberId:creator.creatorId,recipientCreatorNickname:creator.nickname,recipientCreatorProfileImageUrl:creator.profileImageUrl,/), 'header must forward creator.profileImageUrl')
|
||||||
|
check.call(action.include?('let recipientCreatorProfileImageUrl:String') && action.include?('.giftSend(recipientMemberId:recipientMemberId,recipientCreatorNickname:recipientCreatorNickname,recipientCreatorProfileImageUrl:recipientCreatorProfileImageUrl)'), 'action must forward profile URL to AppStep')
|
||||||
|
check.call(content.include?('case .giftSend(let recipientMemberId,let recipientCreatorNickname,let recipientCreatorProfileImageUrl):') && content.include?('GiftSendView(recipientMemberId:recipientMemberId,recipientCreatorNickname:recipientCreatorNickname,recipientCreatorProfileImageUrl:recipientCreatorProfileImageUrl)'), 'ContentView must forward profile URL to GiftSendView')
|
||||||
|
check.call(send_view.include?('let recipientCreatorProfileImageUrl:String') && send_view.include?('init(recipientMemberId:Int,recipientCreatorNickname:String,recipientCreatorProfileImageUrl:String)') && send_view.include?('self.recipientCreatorProfileImageUrl = recipientCreatorProfileImageUrl'), 'GiftSendView must retain display-only profile URL')
|
||||||
|
check.call(send_view.include?('DownsampledKFImage(url:URL(string:recipientCreatorProfileImageUrl),size:CGSize(width:38,height:38)).clipShape(Circle()).accessibilityHidden(true)'), 'recipient must show circular accessible-hidden 38pt profile image')
|
||||||
|
check.call(send_view.include?('guard case .giftSend(let memberId,let nickname,_)= step else { return false } return memberId == recipientMemberId && nickname == recipientCreatorNickname'), 'replacement guard must compare memberId/nickname and ignore only image')
|
||||||
|
model_init = send_view.split('GiftSendViewModel(', 2).last.split('onCreated:', 2).first
|
||||||
|
check.call(!model_init.include?('recipientCreatorProfileImageUrl'), 'profile URL must not enter ViewModel initializer')
|
||||||
|
Dir.glob(File.join(source_root, '**/*{ViewModel,Api,Repository,Request}*.swift')).each do |path|
|
||||||
|
check.call(!File.read(path).include?('recipientCreatorProfileImageUrl'), 'profile URL must remain outside ViewModel/API/request layers')
|
||||||
|
end
|
||||||
|
puts 'PASS gift-phase11 profile: creator image routing, 38pt display, guarded replacement, display-only boundary'
|
||||||
|
end
|
||||||
|
|
||||||
|
if %w[all policy].include?(ARGV[1])
|
||||||
|
mode = 'policy'
|
||||||
|
constants = read.call('V2/Gift/Constants/GiftConstants.swift')
|
||||||
|
agreement = read.call('V2/Gift/Components/GiftAgreementRow.swift')
|
||||||
|
check.call(constants.include?('static let damagePolicyURL = URL(string:"https://app.notion.com/p/3f227b3596738018ba5dea2e5205293b")'), 'damagePolicyURL must use the approved damage-waiver URL')
|
||||||
|
check.call(constants.include?('static let termsURL = URL(string:"https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3")'), 'existing terms URL must remain unchanged')
|
||||||
|
check.call(agreement.include?('var policyURL:URL? = GiftConstants.termsURL') && agreement.include?('if let url = policyURL { Link(destination:url)'), 'agreement must default to termsURL and link its policyURL')
|
||||||
|
rows = Dir.glob(File.join(source_root, 'V2/Gift/**/*.swift')).flat_map do |path|
|
||||||
|
read.call(path.delete_prefix(source_root + '/')).scan(/GiftAgreementRow\(([^)]*)\)/).flatten
|
||||||
|
end
|
||||||
|
damage_rows, other_rows = rows.partition { |row| row.include?('isAgreed:$viewModel.damageWaiverAgreed') }
|
||||||
|
check.call(damage_rows.length == 1 && damage_rows.first.end_with?('policyURL:GiftConstants.damagePolicyURL'), 'only damage waiver must select damagePolicyURL')
|
||||||
|
check.call(other_rows.length == 4 && other_rows.none? { |row| row.include?('policyURL:') }, 'existing sender/recipient agreements must keep default terms URL')
|
||||||
|
puts 'PASS gift-phase11 policy: isolated damage-waiver URL, unchanged default agreement links'
|
||||||
|
end
|
||||||
|
|
||||||
|
if %w[all button].include?(ARGV[1])
|
||||||
|
mode = 'button'
|
||||||
|
action = read.call('V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift')
|
||||||
|
predicate = action[/private var shouldShowGiftButton:Bool \{[^}]*\}/]
|
||||||
|
check.call(predicate == 'private var shouldShowGiftButton:Bool { let normalizedCountryCode = UserDefaults .string(forKey:.countryCode).trimmingCharacters(in:.whitespacesAndNewlines).uppercased()let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR" return isOwnCreatorChannel == false && isDmAvailable && isKoreanCountry && LanguageHeaderProvider.current == "ko" }', 'gift button visibility predicate must remain unchanged')
|
||||||
|
button = action[/if shouldShowGiftButton \{ (.*?) Spacer\(\)/, 1]
|
||||||
|
check.call(button && button.start_with?('Button(action:{ AppState.shared.setAppStep(step:.giftSend(recipientMemberId:recipientMemberId,recipientCreatorNickname:recipientCreatorNickname,recipientCreatorProfileImageUrl:recipientCreatorProfileImageUrl))}){ '), 'gift button action must preserve all three recipient values')
|
||||||
|
label = button.split('}){ ', 2).last
|
||||||
|
check.call(label.start_with?('HStack(spacing:SodaSpacing.s6){ Image("ic_send_gift").resizable().frame(width:20,height:20).accessibilityHidden(true)Text(I18n.CreatorChannelHome.sendGift)'), 'gift button must place the accessible-hidden 20pt icon before text in an s6 HStack')
|
||||||
|
check.call(label == 'HStack(spacing:SodaSpacing.s6){ Image("ic_send_gift").resizable().frame(width:20,height:20).accessibilityHidden(true)Text(I18n.CreatorChannelHome.sendGift).appFont(size:16,weight:.medium).foregroundColor(.black)} .padding(SodaSpacing.s12).background(Capsule().fill(Color.white))} .buttonStyle(.plain)}', 'gift button must preserve font/padding and use black text on a white Capsule')
|
||||||
|
puts 'PASS gift-phase11 button: icon-before-text, 20pt hidden icon, black text, white Capsule, unchanged visibility/action'
|
||||||
|
end
|
||||||
|
RUBY
|
||||||
Reference in New Issue
Block a user