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
@@ -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

+1 -1
View File
@@ -197,7 +197,7 @@ enum AppStep {
case seriesMain
case giftSend(recipientMemberId: Int, recipientCreatorNickname: String)
case giftSend(recipientMemberId: Int, recipientCreatorNickname: String, recipientCreatorProfileImageUrl: String)
case giftBoxList
+2 -2
View File
@@ -374,8 +374,8 @@ struct AppStepLayerView: View {
case .seriesMain:
SeriesMainView()
case .giftSend(let recipientMemberId, let recipientCreatorNickname):
GiftSendView(recipientMemberId: recipientMemberId, recipientCreatorNickname: recipientCreatorNickname)
case .giftSend(let recipientMemberId, let recipientCreatorNickname, let recipientCreatorProfileImageUrl):
GiftSendView(recipientMemberId: recipientMemberId, recipientCreatorNickname: recipientCreatorNickname, recipientCreatorProfileImageUrl: recipientCreatorProfileImageUrl)
case .giftBoxList:
GiftBoxListView()
@@ -57,6 +57,7 @@ struct CreatorChannelHeaderSection: View {
CreatorChannelTalkActionSection(
recipientMemberId: creator.creatorId,
recipientCreatorNickname: creator.nickname,
recipientCreatorProfileImageUrl: creator.profileImageUrl,
isAiChatAvailable: creator.isAiChatAvailable,
isDmAvailable: creator.isDmAvailable,
isOwnCreatorChannel: isOwnCreatorChannel,
@@ -3,6 +3,7 @@ import SwiftUI
struct CreatorChannelTalkActionSection: View {
let recipientMemberId: Int
let recipientCreatorNickname: String
let recipientCreatorProfileImageUrl: String
let isAiChatAvailable: Bool
let isDmAvailable: Bool
let isOwnCreatorChannel: Bool
@@ -66,16 +67,24 @@ struct CreatorChannelTalkActionSection: View {
Button(action: {
AppState.shared.setAppStep(step: .giftSend(
recipientMemberId: recipientMemberId,
recipientCreatorNickname: recipientCreatorNickname
recipientCreatorNickname: recipientCreatorNickname,
recipientCreatorProfileImageUrl: recipientCreatorProfileImageUrl
))
}) {
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(.white)
.foregroundColor(.black)
}
.padding(SodaSpacing.s12)
.overlay(
.background(
Capsule()
.stroke(Color.white.opacity(0.3), lineWidth: 1)
.fill(Color.white)
)
}
.buttonStyle(.plain)
@@ -3,6 +3,7 @@ import SwiftUI
struct GiftAgreementRow: View {
let title: String
@Binding var isAgreed: Bool
var policyURL: URL? = GiftConstants.termsURL
var body: some View {
HStack(alignment: .top, spacing: SodaSpacing.s4) {
@@ -25,7 +26,7 @@ struct GiftAgreementRow: View {
.buttonStyle(.plain)
.accessibilityValue(isAgreed ? "동의함" : "동의 안 함")
if let url = GiftConstants.termsURL {
if let url = policyURL {
Link(destination: url) {
Text("(필수)")
.underline()
@@ -2,6 +2,7 @@ import Foundation
enum GiftConstants {
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 inquiryStep: AppStep = .serviceCenter
static let feedbackStep: AppStep = .serviceCenter
@@ -1,10 +1,12 @@
import SwiftUI
struct GiftSendView: View {
private let recipientCreatorProfileImageUrl: String
@StateObject private var viewModel: GiftSendViewModel
@State private var hasAppeared = false
init(recipientMemberId: Int, recipientCreatorNickname: String) {
init(recipientMemberId: Int, recipientCreatorNickname: String, recipientCreatorProfileImageUrl: String) {
self.recipientCreatorProfileImageUrl = recipientCreatorProfileImageUrl
_viewModel = StateObject(wrappedValue: GiftSendViewModel(
recipientMemberId: recipientMemberId,
recipientCreatorNickname: recipientCreatorNickname,
@@ -12,7 +14,7 @@ struct GiftSendView: View {
AppState.shared.replaceCurrentAppStep(
step: .giftDetail(applicationNo: applicationNo),
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
}
)
@@ -77,7 +79,8 @@ struct GiftSendView: View {
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
sectionTitle("받는 크리에이터")
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)
Text(viewModel.recipientCreatorNickname)
.appFont(size: 16, weight: .bold)
@@ -155,7 +158,8 @@ struct GiftSendView: View {
}
.disabled(viewModel.categories.isEmpty)
if viewModel.selectedCategory?.requiresDamageWaiver == true {
GiftAgreementRow(title: "파손 및 분실 면책 사항에 동의합니다.", isAgreed: $viewModel.damageWaiverAgreed)
GiftAgreementRow(title: "파손 및 분실 면책 사항에 동의합니다.", isAgreed: $viewModel.damageWaiverAgreed,
policyURL: GiftConstants.damagePolicyURL)
}
}
}
+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