Files
sodalive-ios/SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift
Yu Sung 91474b48b5 feat(original): 작품별 상세 UI 변경
- 캐릭터 / 작품 정보 탭 추가
- 작품 정보 탭 구성
  - 작품 소개
  - 원작 보러 가기
  - 상세 정보
    - 작가
    - 제작사
    - 원작
2025-09-23 14:32:16 +09:00

271 lines
11 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OriginalWorkDetailView.swift
// SodaLive
//
// Created by klaus on 9/15/25.
//
import SwiftUI
import Kingfisher
struct OriginalWorkDetailView: View {
@StateObject var viewModel = OriginalWorkDetailViewModel()
let originalId: Int
var body: some View {
NavigationStack {
BaseView(isLoading: $viewModel.isLoading) {
ZStack(alignment: .top) {
if let imageUrl = viewModel.response?.imageUrl {
KFImage(URL(string: imageUrl))
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.blur(radius: 25)
}
Color.black.opacity(0.5).ignoresSafeArea()
VStack(spacing: 0) {
HStack(spacing: 0) {
Image("ic_back")
.resizable()
.frame(width: 24, height: 24)
.onTapGesture {
AppState.shared.back()
}
Spacer()
}
.padding(.horizontal, 24)
.frame(height: 56)
if let response = viewModel.response {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
OriginalWorkDetailHeaderView(item: response)
.padding(.horizontal, 24)
.padding(.bottom, 24)
HStack(spacing: 0) {
SeriesDetailTabView(
title: "ìº<EFBFBD>릭터",
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .character
) {
if viewModel.currentTab != .character {
viewModel.currentTab = .character
}
}
SeriesDetailTabView(
title: "작품정보",
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info
) {
if viewModel.currentTab != .info {
viewModel.currentTab = .info
}
}
}
.background(Color.black)
Rectangle()
.foregroundColor(Color.gray90.opacity(0.5))
.frame(height: 1)
.frame(maxWidth: .infinity)
switch(viewModel.currentTab) {
case .info:
OriginalWorkInfoView(response: response)
default:
OriginalWorkCharacterView(characters: viewModel.characters)
}
}
}
}
}
}
}
.onAppear {
if viewModel.response == nil {
viewModel.originalId = originalId
}
}
.navigationDestination(for: Int.self) { characterId in
CharacterDetailView(characterId: characterId)
}
}
}
}
struct OriginalWorkCharacterView: View {
private let horizontalPadding: CGFloat = 12
private let gridSpacing: CGFloat = 12
let characters: [Character]
var body: some View {
ZStack {
let totalSpacing: CGFloat = gridSpacing * 2
let width = (screenSize().width - (horizontalPadding * 2) - totalSpacing) / 3
LazyVGrid(
columns: Array(
repeating: GridItem(
.flexible(),
spacing: gridSpacing,
alignment: .topLeading
),
count: 3
),
alignment: .leading,
spacing: gridSpacing
) {
ForEach(characters.indices, id: \.self) { idx in
let item = characters[idx]
NavigationLink(value: item.characterId) {
CharacterItemView(
character: item,
size: width,
rank: 0,
isShowRank: false
)
}
}
}
.padding(.horizontal, horizontalPadding)
}
.padding(.top, 24)
.background(Color.black)
}
}
struct OriginalWorkInfoView: View {
let response: OriginalWorkDetailResponse
@State private var isExpandDesc = false
var body: some View {
ZStack {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text("작품 소개")
.font(.custom(Font.preBold.rawValue, size: 16))
.foregroundColor(.white)
Text(response.description)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
.lineLimit(isExpandDesc ? Int.max : 3)
.truncationMode(.tail)
.onTapGesture {
isExpandDesc.toggle()
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(hex: "263238"))
.cornerRadius(16)
VStack(alignment: .leading, spacing: 8) {
Text("ì<EFBFBD>ìž ë³´ëŸ¬ 가기")
.font(.custom(Font.preBold.rawValue, size: 16))
.foregroundColor(Color(hex: "B0BEC5"))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(0..<response.originalLinks.count, id: \.self) {
let link = response.originalLinks[$0]
Text(link)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(.white)
.onTapGesture {
if let url = URL(string: link) {
UIApplication.shared.open(url)
}
}
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(hex: "263238"))
.cornerRadius(16)
VStack(alignment: .leading, spacing: 8) {
Text("ìƒ<EFBFBD>세 ì •ë³´")
.font(.custom(Font.preBold.rawValue, size: 16))
.foregroundColor(.white)
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
if let _ = response.writer {
Text("작가")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
}
if let _ = response.studio {
Text("제작사")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
}
if let _ = response.originalWork {
Text("ì<EFBFBD>ìž")
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(Color(hex: "B0BEC5"))
}
}
VStack(alignment: .leading, spacing: 8) {
if let writer = response.writer {
Text(writer)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(.white)
}
if let studio = response.studio {
Text(studio)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(.white)
}
if let originalWork = response.originalWork {
Text(originalWork)
.font(.custom(Font.preRegular.rawValue, size: 14))
.foregroundColor(.white)
.underline(response.originalLink != nil ? true : false)
.onTapGesture {
if let link = response.originalLink, let url = URL(string: link) {
UIApplication.shared.open(url)
}
}
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(hex: "263238"))
.cornerRadius(16)
}
}
.padding(24)
.frame(maxWidth: .infinity)
.background(Color.black)
}
}
#Preview {
OriginalWorkDetailView(originalId: 0)
}