diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift index 3afee2d..a397b7a 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionView.swift @@ -33,21 +33,51 @@ struct AddSubscriptionView: View { colorScheme: colorScheme ) - InputFieldSection( - title: "키워드 필터", - placeholder: "장학금, 교직, 학생회", - description: "관심 키워드가 포함된 내용을 걸러낼 필요가 있으면 입력하세요.", - text: $viewModel.keywordsText, - colorScheme: colorScheme, - additionalContent: { - AnyView( - HStack(spacing: 8) { - KeywordBadge(text: "장학금", colorScheme: colorScheme) - KeywordBadge(text: "교직부공지사항", colorScheme: colorScheme) + VStack(alignment: .leading, spacing: 12) { + // 키워드 필터 섹션 + VStack(alignment: .leading, spacing: 8) { + Text("키워드 필터") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(Color.primaryGreen) + + // 키워드 추가 버튼 + Button(action: { + viewModel.showKeywordSelector = true + }) { + HStack { + Text("키워드 추가...") + .font(.system(size: 16)) + .foregroundColor(Color.secondaryText(colorScheme)) + Spacer() } - ) + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondaryBackground(colorScheme)) + ) + } + + Text("관심 키워드가 포함된 내용을 걸러낼 필요가 있으면 입력하세요.") + .font(.system(size: 12)) + .foregroundColor(Color.secondaryText(colorScheme)) + .fixedSize(horizontal: false, vertical: true) } - ) + + // 선택된 키워드 배지들 + if !viewModel.selectedKeywords.isEmpty { + FlowLayout(spacing: 8) { + ForEach(viewModel.selectedKeywords, id: \.self) { keyword in + KeywordBadgeWithDelete( + text: keyword, + colorScheme: colorScheme + ) { + viewModel.removeKeyword(keyword) + } + } + } + } + } UrgentToggleRow(isOn: $viewModel.isUrgent, colorScheme: colorScheme) @@ -72,6 +102,171 @@ struct AddSubscriptionView: View { } } } + .sheet(isPresented: $viewModel.showKeywordSelector) { + KeywordSelectorSheet(viewModel: viewModel, colorScheme: colorScheme) + } + } +} + +// 키워드 선택 시트 +struct KeywordSelectorSheet: View { + @ObservedObject var viewModel: AddSubscriptionViewModel + let colorScheme: ColorScheme + @Environment(\.dismiss) var dismiss + + var body: some View { + ZStack { + Color.background(colorScheme) + .ignoresSafeArea() + + VStack(spacing: 0) { + // 헤더 + HStack { + Spacer() + Text("구독 설정") + .font(.custom("KoddiUD OnGothic Bold", size: 24)) + .foregroundColor(Color.text(colorScheme)) + Spacer() + Button(action: { + dismiss() + }) { + Image(systemName: "xmark") + .font(.system(size: 20)) + .foregroundColor(Color.text(colorScheme)) + } + } + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 32) + + // 키워드 설정 섹션 + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("키워드 설정") + .font(.custom("KoddiUD OnGothic Bold", size: 20)) + .foregroundColor(Color.primaryGreen) + + Spacer() + } + .padding(.horizontal, 20) + + // 키워드 체크박스 리스트 + VStack(spacing: 0) { + ForEach(Array(viewModel.availableKeywords.enumerated()), id: \.offset) { index, keyword in + KeywordCheckboxRow( + keyword: keyword, + isSelected: viewModel.selectedKeywords.contains(keyword), + colorScheme: colorScheme + ) { + viewModel.toggleKeyword(keyword) + } + + if index < viewModel.availableKeywords.count - 1 { + Divider() + .background(Color.border(colorScheme)) + .padding(.horizontal, 20) + } + } + } + } + + Spacer() + + // 저장하기 버튼 + Button(action: { + dismiss() + }) { + Text("저장하기") + .font(.custom("KoddiUD OnGothic Bold", size: 18)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Color.primaryGreen) + .cornerRadius(12) + } + .padding(.horizontal, 20) + .padding(.bottom, 34) + } + } + } +} + +// 삭제 가능한 키워드 배지 +struct KeywordBadgeWithDelete: View { + let text: String + let colorScheme: ColorScheme + let onDelete: () -> Void + + var body: some View { + HStack(spacing: 6) { + Text(text) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + + Button(action: onDelete) { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.primaryGreen) + ) + } +} + +// FlowLayout for keywords +struct FlowLayout: Layout { + var spacing: CGFloat = 8 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let result = FlowResult( + in: proposal.replacingUnspecifiedDimensions().width, + subviews: subviews, + spacing: spacing + ) + return result.size + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + let result = FlowResult( + in: bounds.width, + subviews: subviews, + spacing: spacing + ) + for (index, subview) in subviews.enumerated() { + subview.place(at: CGPoint(x: bounds.minX + result.positions[index].x, y: bounds.minY + result.positions[index].y), proposal: .unspecified) + } + } + + struct FlowResult { + var size: CGSize = .zero + var positions: [CGPoint] = [] + + init(in maxWidth: CGFloat, subviews: Subviews, spacing: CGFloat) { + var currentX: CGFloat = 0 + var currentY: CGFloat = 0 + var lineHeight: CGFloat = 0 + + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + + if currentX + size.width > maxWidth, currentX > 0 { + currentX = 0 + currentY += lineHeight + spacing + lineHeight = 0 + } + + positions.append(CGPoint(x: currentX, y: currentY)) + lineHeight = max(lineHeight, size.height) + currentX += size.width + spacing + } + + size = CGSize(width: maxWidth, height: currentY + lineHeight) + } } } diff --git a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift index f5f1a77..d3552eb 100644 --- a/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift +++ b/today-s-sound/Presentation/Features/AddSubscription/AddSubscriptionViewModel.swift @@ -4,6 +4,28 @@ import Foundation class AddSubscriptionViewModel: ObservableObject { @Published var urlText: String = "" @Published var nameText: String = "" - @Published var keywordsText: String = "" @Published var isUrgent: Bool = false + @Published var selectedKeywords: [String] = [] + @Published var showKeywordSelector: Bool = false + + let availableKeywords = ["장애인", "긴급속보", "장학금", "교직부공지사항", "학생회", "도서관"] + + func addKeyword(_ keyword: String) { + let trimmed = keyword.trimmingCharacters(in: .whitespaces) + if !trimmed.isEmpty, !selectedKeywords.contains(trimmed) { + selectedKeywords.append(trimmed) + } + } + + func removeKeyword(_ keyword: String) { + selectedKeywords.removeAll { $0 == keyword } + } + + func toggleKeyword(_ keyword: String) { + if selectedKeywords.contains(keyword) { + removeKeyword(keyword) + } else { + addKeyword(keyword) + } + } } diff --git a/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift new file mode 100644 index 0000000..2aa6470 --- /dev/null +++ b/today-s-sound/Presentation/Features/AddSubscription/Component/KeywordCheckboxRow.swift @@ -0,0 +1,49 @@ +// +// KeywordCheckboxRow.swift +// today-s-sound +// +// Created by Assistant on 12/19/24. +// + +import SwiftUI + +// 키워드 체크박스 Row 컴포넌트 +struct KeywordCheckboxRow: View { + let keyword: String + let isSelected: Bool + let colorScheme: ColorScheme + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: 16) { + // 체크박스 + ZStack { + RoundedRectangle(cornerRadius: 6) + .stroke(isSelected ? Color.primaryGreen : Color.border(colorScheme), lineWidth: 2) + .frame(width: 28, height: 28) + + if isSelected { + RoundedRectangle(cornerRadius: 6) + .fill(Color.primaryGreen) + .frame(width: 28, height: 28) + + Image(systemName: "checkmark") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + } + } + + // 키워드 텍스트 + Text(keyword) + .font(.custom("KoddiUD OnGothic Regular", size: 18)) + .foregroundColor(Color.text(colorScheme)) + + Spacer() + } + .padding(.vertical, 16) + .padding(.horizontal, 20) + } + .buttonStyle(PlainButtonStyle()) + } +}