feat(gift): 선물 발송 표시를 보정한다

This commit is contained in:
Yu Sung
2026-10-08 14:13:35 +09:00
parent e6803bc2c8
commit 24d2508700
10 changed files with 119 additions and 14 deletions
+68
View File
@@ -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