From f0763d75c29a7feeeb08a41598a415a916122e2a Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Wed, 4 Mar 2026 17:30:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(community):=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=84=EC=B2=B4=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?=EB=A7=90=EC=A4=84=EC=9E=84=EA=B3=BC=20=ED=8F=B0=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EB=A0=AC,=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=95=EC=9E=A5=20=EB=8F=99=EC=9E=91=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../All/CreatorCommunityAllItemView.swift | 120 +++++++++++++++--- ...04_커뮤니티전체아이템텍스트확장토글구현.md | 100 +++++++++++++++ 2 files changed, 199 insertions(+), 21 deletions(-) create mode 100644 docs/20260304_커뮤니티전체아이템텍스트확장토글구현.md diff --git a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift index 89744f5..ff891e4 100644 --- a/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift +++ b/SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllItemView.swift @@ -5,6 +5,7 @@ // Created by klaus on 2023/12/15. // +import Foundation import SwiftUI import Kingfisher import SDWebImageSwiftUI @@ -20,7 +21,9 @@ struct CreatorCommunityAllItemView: View { @State var isLike = false @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 contentPlayManager = ContentPlayManager.shared @@ -55,11 +58,11 @@ struct CreatorCommunityAllItemView: View { VStack(alignment: .leading, spacing: 3) { Text(item.creatorNickname) - .appFont(size: 13.3, weight: .medium) + .appFont(size: 18, weight: .bold) .foregroundColor(Color.grayee) Text(item.relativeTimeText()) - .appFont(size: 13.3, weight: .light) + .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray77) } .padding(.leading, 11) @@ -73,22 +76,43 @@ struct CreatorCommunityAllItemView: View { } } - DetectableTextView(text: item.content, textSize: 13.3, font: Font.preMedium.rawValue) - .frame( - width: screenSize().width - 42, - height: textHeight + Group { + if isContentExpanded { + Text(linkedAttributedContent(from: item.content)) + } 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 { - self.textHeight = self.estimatedHeight( - for: item.content, - width: screenSize().width - 42 - ) + let width = contentTextWidth > 0 ? contentTextWidth : (screenSize().width - 42) + updateContentTruncationState(for: item.content, width: width) } .onChange(of: item.content) { newText in - self.textHeight = self.estimatedHeight( - for: newText, - width: screenSize().width - 42 - ) + isContentExpanded = false + let width = contentTextWidth > 0 ? contentTextWidth : (screenSize().width - 42) + updateContentTruncationState(for: newText, width: width) } if item.price <= 0 || item.existOrdered { @@ -97,6 +121,7 @@ struct CreatorCommunityAllItemView: View { WebImage(url: URL(string: imageUrl)) .resizable() .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) .clipped() if let audioUrl = item.audioUrl { @@ -149,16 +174,69 @@ struct CreatorCommunityAllItemView: View { } .padding(.horizontal, 8) .padding(.vertical, 11) - .background(Color.gray22) - .cornerRadius(5.3) + .frame(maxWidth: .infinity) + .background(Color(hex: "263238")) + .cornerRadius(16) .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 { - let textView = UITextView(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude)) - textView.font = UIFont.systemFont(ofSize: 13.3) - textView.text = text - return textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: Font.preRegular.rawValue, size: 18) ?? UIFont.systemFont(ofSize: 18, weight: .regular) + ] + + 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 } } diff --git a/docs/20260304_커뮤니티전체아이템텍스트확장토글구현.md b/docs/20260304_커뮤니티전체아이템텍스트확장토글구현.md new file mode 100644 index 0000000..1f508ba --- /dev/null +++ b/docs/20260304_커뮤니티전체아이템텍스트확장토글구현.md @@ -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.`로 실행 불가.