fix(community): 커뮤니티 전체 아이템 말줄임과 폰트를 정렬, 텍스트 확장 동작을 개선한다

This commit is contained in:
Yu Sung
2026-03-04 17:30:28 +09:00
parent 9d6f0c648b
commit f0763d75c2
2 changed files with 199 additions and 21 deletions

View File

@@ -5,6 +5,7 @@
// Created by klaus on 2023/12/15. // Created by klaus on 2023/12/15.
// //
import Foundation
import SwiftUI import SwiftUI
import Kingfisher import Kingfisher
import SDWebImageSwiftUI import SDWebImageSwiftUI
@@ -20,7 +21,9 @@ struct CreatorCommunityAllItemView: View {
@State var isLike = false @State var isLike = false
@State var likeCount = 0 @State var likeCount = 0
@State private var textHeight: CGFloat = .zero @State private var isContentExpanded = false
@State private var isContentTruncated = false
@State private var contentTextWidth: CGFloat = 0
@StateObject var playManager = CreatorCommunityMediaPlayerManager.shared @StateObject var playManager = CreatorCommunityMediaPlayerManager.shared
@StateObject var contentPlayManager = ContentPlayManager.shared @StateObject var contentPlayManager = ContentPlayManager.shared
@@ -55,11 +58,11 @@ struct CreatorCommunityAllItemView: View {
VStack(alignment: .leading, spacing: 3) { VStack(alignment: .leading, spacing: 3) {
Text(item.creatorNickname) Text(item.creatorNickname)
.appFont(size: 13.3, weight: .medium) .appFont(size: 18, weight: .bold)
.foregroundColor(Color.grayee) .foregroundColor(Color.grayee)
Text(item.relativeTimeText()) Text(item.relativeTimeText())
.appFont(size: 13.3, weight: .light) .appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray77) .foregroundColor(Color.gray77)
} }
.padding(.leading, 11) .padding(.leading, 11)
@@ -73,22 +76,43 @@ struct CreatorCommunityAllItemView: View {
} }
} }
DetectableTextView(text: item.content, textSize: 13.3, font: Font.preMedium.rawValue) Group {
.frame( if isContentExpanded {
width: screenSize().width - 42, Text(linkedAttributedContent(from: item.content))
height: textHeight } else {
Text(item.content)
.lineLimit(3)
.truncationMode(.tail)
}
}
.appFont(size: 18, weight: .regular)
.foregroundColor(Color(hex: "B0BEC5"))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.background(
GeometryReader { proxy in
Color.clear
.onAppear {
updateContentWidth(proxy.size.width)
}
.onChange(of: proxy.size.width) { newWidth in
updateContentWidth(newWidth)
}
}
) )
.onTapGesture {
guard isContentTruncated || isContentExpanded else { return }
isContentExpanded.toggle()
}
.onAppear { .onAppear {
self.textHeight = self.estimatedHeight( let width = contentTextWidth > 0 ? contentTextWidth : (screenSize().width - 42)
for: item.content, updateContentTruncationState(for: item.content, width: width)
width: screenSize().width - 42
)
} }
.onChange(of: item.content) { newText in .onChange(of: item.content) { newText in
self.textHeight = self.estimatedHeight( isContentExpanded = false
for: newText, let width = contentTextWidth > 0 ? contentTextWidth : (screenSize().width - 42)
width: screenSize().width - 42 updateContentTruncationState(for: newText, width: width)
)
} }
if item.price <= 0 || item.existOrdered { if item.price <= 0 || item.existOrdered {
@@ -97,6 +121,7 @@ struct CreatorCommunityAllItemView: View {
WebImage(url: URL(string: imageUrl)) WebImage(url: URL(string: imageUrl))
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.clipped() .clipped()
if let audioUrl = item.audioUrl { if let audioUrl = item.audioUrl {
@@ -149,16 +174,69 @@ struct CreatorCommunityAllItemView: View {
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 11) .padding(.vertical, 11)
.background(Color.gray22) .frame(maxWidth: .infinity)
.cornerRadius(5.3) .background(Color(hex: "263238"))
.cornerRadius(16)
.padding(.horizontal, 13.3) .padding(.horizontal, 13.3)
} }
private func updateContentWidth(_ width: CGFloat) {
guard width > 0 else { return }
contentTextWidth = width
updateContentTruncationState(for: item.content, width: width)
}
private func updateContentTruncationState(for text: String, width: CGFloat) {
let fullHeight = estimatedHeight(for: text, width: width)
let collapsedHeight = estimatedCollapsedHeight(lineLimit: 3)
isContentTruncated = fullHeight > (collapsedHeight + 0.5)
}
private func estimatedCollapsedHeight(lineLimit: Int) -> CGFloat {
let font = UIFont(name: Font.preRegular.rawValue, size: 18) ?? UIFont.systemFont(ofSize: 18, weight: .regular)
let lineCount = CGFloat(lineLimit)
return font.lineHeight * lineCount
}
private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat { private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat {
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude)) let attributes: [NSAttributedString.Key: Any] = [
textView.font = UIFont.systemFont(ofSize: 13.3) .font: UIFont(name: Font.preRegular.rawValue, size: 18) ?? UIFont.systemFont(ofSize: 18, weight: .regular)
textView.text = text ]
return textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
let rect = NSAttributedString(string: text, attributes: attributes)
.boundingRect(
with: CGSize(width: width, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.height)
}
private func linkedAttributedContent(from text: String) -> AttributedString {
var attributedText = AttributedString(text)
guard
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
else {
return attributedText
}
let nsText = text as NSString
let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
for match in matches {
guard
let url = match.url,
let range = Range(match.range, in: attributedText)
else {
continue
}
attributedText[range].link = url
}
return attributedText
} }
} }

View File

@@ -0,0 +1,100 @@
# 20260304 커뮤니티 전체 아이템 텍스트 확장 토글 구현
## 구현 목표
- `CreatorCommunityAllItemView`의 닉네임/콘텐츠/날짜 글자 크기와 weight를 `CreatorCommunityItemView`와 동일하게 맞춘다.
- 긴 콘텐츠에서 내부 스크롤이 생기지 않도록 말줄임표 + 탭 확장/축소 토글을 적용한다.
- 콘텐츠가 확장되면 아이템 전체 높이가 늘어나도록 구성한다.
- 텍스트 잘림 판별 로직을 SwiftUI 리스트 성능 관점에서 더 효율적인 방식으로 유지/개선한다.
## 체크리스트
- [x] 기존 `CreatorCommunity` 텍스트 스타일/패턴 탐색
- [x] `CreatorCommunityItemView`와 동일한 텍스트 size/weight 적용(닉네임/내용/날짜)
- [x] `CreatorCommunityAllItemView`의 콘텐츠 텍스트를 말줄임표 + 탭 확장/축소로 변경
- [x] 내부 스크롤 제거 및 아이템 높이 확장 동작 반영
- [x] 텍스트 잘림 판별 로직 효율성 점검 및 개선
- [x] 수정 파일 진단 및 빌드 검증
## 검증 기록
- 2026-03-04
- 무엇: 작업 시작 및 구현 계획 수립
- 왜: 긴 콘텐츠에서 콘텐츠 영역만 스크롤되는 UX 이슈를 개선하기 위해
- 어떻게: 관련 뷰/텍스트 컴포넌트 탐색 후 `CreatorCommunityAllItemView`를 최소 변경으로 수정 예정
- 결과: 완료
- 2026-03-04 (구현/검증 완료)
- 무엇: `CreatorCommunityAllItemView`에서 콘텐츠 표시를 `DetectableTextView` 고정 높이 방식에서 `Text` 기반 말줄임/탭 확장 토글 방식으로 변경했다. 닉네임/콘텐츠/날짜의 글자 크기(13.3)와 콘텐츠 폰트(`.appFont(size: 13.3, weight: .medium)`)를 유지했다.
- 왜: 긴 텍스트에서 콘텐츠 영역만 스크롤되는 UX를 제거하고, 터치 시 아이템 전체 높이가 자연스럽게 확장/축소되도록 하기 위해.
- 어떻게:
- 수정 파일
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift`
- 적용 내용
- `@State``isContentExpanded`/`isContentTruncated` 상태 추가
- 콘텐츠 `Text``lineLimit(3)` + `truncationMode(.tail)` 적용
- 콘텐츠 탭 시 길이가 긴 경우에만 확장/축소 토글
- `UITextView.sizeThatFits` 기반 높이 계산으로 잘림 여부 판단(내부 스크롤 비활성화)
- 실행 명령
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- `lsp_diagnostics` (`CreatorCommunityAllItemView.swift`)
- 결과:
- 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **` 확인.
- 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 실행 불가(프로젝트 설정 이슈).
- LSP: 로컬 SourceKit 환경에서 `No such module 'Kingfisher'` 진단이 있으나, 실제 Xcode 빌드는 성공.
- 2026-03-04 (요구사항 변경 반영: `CreatorCommunityItemView`와 폰트 정렬 + 효율 개선)
- 무엇: `CreatorCommunityAllItemView`의 텍스트 스타일을 닉네임 `18/bold`, 날짜 `14/regular`, 콘텐츠 `18/regular`로 변경하고, 콘텐츠는 3줄 말줄임 + 탭 확장/축소를 유지했다.
- 왜: 사용자 요청대로 기준 뷰(`CreatorCommunityItemView`)와 타이포그래피를 통일하고, 기존 `UITextView` 인스턴스 기반 측정보다 가벼운 잘림 판별 방식으로 최적화하기 위해.
- 어떻게:
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift`
- 콘텐츠 텍스트 스타일을 `CreatorCommunityItemView` 기준으로 변경
- `GeometryReader`로 실제 렌더링 폭을 반영해 잘림 판별
- 잘림 판별을 `UITextView.sizeThatFits`에서 `NSAttributedString.boundingRect`로 교체
- 길이 초과 시에만 탭 토글 허용, 확장 시 아이템 전체 높이 증가
- 실행 명령
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- `lsp_diagnostics` (`CreatorCommunityAllItemView.swift`)
- 결과:
- 빌드: 두 스킴 모두 `** BUILD SUCCEEDED **`.
- 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`.
- LSP: SourceKit 로컬 환경에서 `No such module 'Kingfisher'` 1건(환경성), 실제 빌드 통과.
- 2026-03-04 (요구사항 추가 반영: 이미지 라운드 코너)
- 무엇: `CreatorCommunityAllItemView``WebImage` 표시부에 `cornerRadius 8`을 SwiftUI 방식으로 적용했다.
- 왜: iOS 16에서 가장 SwiftUI 코드답게 이미지 라운드 모서리를 처리하기 위해.
- 어떻게:
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift`
- `WebImage` 체인에 `.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))` 추가
- 실행 명령(검증 예정)
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- `lsp_diagnostics` (`CreatorCommunityAllItemView.swift`)
- 결과:
- LSP: SourceKit 로컬 환경에서 `No such module 'Kingfisher'` 1건(환경성).
- 빌드: `SodaLive-dev``** BUILD SUCCEEDED **`, `SodaLive`는 동시 빌드 중 `build.db is locked` 1회 발생 후 순차 재실행으로 `** BUILD SUCCEEDED **` 확인.
- 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 실행 불가.
- 2026-03-04 (요구사항 추가 반영: 펼침 상태 URL 탭)
- 무엇: 콘텐츠가 펼쳐진 상태에서만 `https` 링크를 탭할 수 있도록 적용했다.
- 왜: 접힘 상태(말줄임)에서는 기존 토글 UX를 유지하고, 펼침 상태에서만 링크 이동을 허용하기 위해.
- 어떻게:
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift`
- 콘텐츠 뷰를 분기: 접힘은 `Text(item.content)` + `lineLimit(3)` 유지, 펼침은 `Text(AttributedString)` 사용
- `NSDataDetector(.link)`로 URL 범위를 검출해 `AttributedString``.link` 속성 주입
- 기존 토글 상태(`isContentExpanded`)와 잘림 판별(`isContentTruncated`) 로직 유지
- 실행 명령(검증 예정)
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- `lsp_diagnostics` (`CreatorCommunityAllItemView.swift`)
- 결과:
- LSP: SourceKit 로컬 환경에서 `No such module 'Kingfisher'` 1건(환경성).
- 빌드: `SodaLive`, `SodaLive-dev` 모두 `** BUILD SUCCEEDED **` 확인.
- 테스트: 두 스킴 모두 `Scheme ... is not currently configured for the test action.`로 실행 불가.