Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
13 changes: 11 additions & 2 deletions BusRoad.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
objects = {

/* Begin PBXBuildFile section */
60043BAB2E8AC91300167A28 /* Secrets.plist in Resources */ = {isa = PBXBuildFile; fileRef = 60043BAA2E8AC91300167A28 /* Secrets.plist */; };
60A8CDB02E7CE7B900530B92 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 60A8CDAF2E7CE7A100530B92 /* .swiftlint.yml */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
60043BAA2E8AC91300167A28 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secrets.plist; sourceTree = "<group>"; };
60A8CDA12E7CDFE500530B92 /* BusRoad.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BusRoad.app; sourceTree = BUILT_PRODUCTS_DIR; };
60A8CDAF2E7CE7A100530B92 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -51,6 +53,7 @@
60A8CD982E7CDFE500530B92 = {
isa = PBXGroup;
children = (
60043BAA2E8AC91300167A28 /* Secrets.plist */,
60A8CDAF2E7CE7A100530B92 /* .swiftlint.yml */,
60A8CDA32E7CDFE500530B92 /* BusRoad */,
60A8CDA22E7CDFE500530B92 /* Products */,
Expand All @@ -75,7 +78,7 @@
60A8CD9D2E7CDFE500530B92 /* Sources */,
60A8CD9E2E7CDFE500530B92 /* Frameworks */,
60A8CD9F2E7CDFE500530B92 /* Resources */,
60A8CDB12E7CE82300530B92 /* ShellScript */,
60A8CDB12E7CE82300530B92 /* Run Script */,
);
buildRules = (
);
Expand Down Expand Up @@ -130,14 +133,15 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
60043BAB2E8AC91300167A28 /* Secrets.plist in Resources */,
60A8CDB02E7CE7B900530B92 /* .swiftlint.yml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
60A8CDB12E7CE82300530B92 /* ShellScript */ = {
60A8CDB12E7CE82300530B92 /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
Expand All @@ -148,6 +152,7 @@
inputPaths = (
"$(SRCROOT)/.swiftlint.yml",
);
name = "Run Script";
outputFileListPaths = (
);
outputPaths = (
Expand Down Expand Up @@ -304,6 +309,8 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusRoad/Info.plist;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "\"음성 검색 기능을 사용하기 위해 마이크 접근 권한이 필요합니다.\"";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "\"말씀하신 내용을 텍스트로 변환하기 위해 음성 인식 권한이 필요합니다.\"";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -342,6 +349,8 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BusRoad/Info.plist;
INFOPLIST_KEY_NSMicrophoneUsageDescription = "\"음성 검색 기능을 사용하기 위해 마이크 접근 권한이 필요합니다.\"";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "\"말씀하신 내용을 텍스트로 변환하기 위해 음성 인식 권한이 필요합니다.\"";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down
97 changes: 97 additions & 0 deletions BusRoad/Component/MainSearchView/PlaceCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import SwiftUI

struct PlaceCard: View {
let title: String
let address: String
var searchQuery: String?
var onTap: (() -> Void)? // 카드 탭 액션

var body: some View {
Button(action: { onTap?() }) {
VStack(alignment: .leading, spacing: 6) {

if let query = searchQuery, !query.isEmpty {
Text(title.highlightedText(searchQuery: query))
.lineLimit(1)
.truncationMode(.tail)
} else {
Text(title)
.font(.headline)
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.tail)
}

Text(address)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.tail)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.white))
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(.separator), lineWidth: 0.5)
)
.shadow(color: Color.black.opacity(0.04), radius: 3, x: 0, y: 1)
}
.buttonStyle(.plain)
.contentShape(RoundedRectangle(cornerRadius: 12))
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(title), \(address)")
}
}

// MARK: - 텍스트 하이라이트 헬퍼
extension String {
/// 검색어와 일치하는 부분을 찾아서 AttributedString으로 변환
func highlightedText(searchQuery: String, highlightColor: Color = .green) -> AttributedString {
var attributedString = AttributedString(self)

guard !searchQuery.isEmpty else { return attributedString }

let lowercasedText = self.lowercased()
let lowercasedQuery = searchQuery.lowercased()

var searchStartIndex = lowercasedText.startIndex

while let range = lowercasedText.range(of: lowercasedQuery, range: searchStartIndex..<lowercasedText.endIndex) {
// AttributedString의 범위로 변환
if let attributedRange = Range(range, in: attributedString) {
// 하이라이트 색상 및 굵기 적용
attributedString[attributedRange].foregroundColor = highlightColor
attributedString[attributedRange].font = .system(.headline, design: .default, weight: .bold)
}

// 다음 검색을 위해 시작 인덱스 업데이트
searchStartIndex = range.upperBound
}

return attributedString
}
}

#Preview {
VStack(spacing: 12) {
PlaceCard(
title: "포항 영일대해수욕장",
address: "경북 포항시 북구 두호동 685",
searchQuery: "포항"
)
PlaceCard(
title: "테라로사 포스텍점",
address: "포항시 남구 청암로 87",
searchQuery: "포항"
)
PlaceCard(
title: "일반 카드 (하이라이트 없음)",
address: "일반 주소"
)
}
.padding()
}
60 changes: 60 additions & 0 deletions BusRoad/Component/MainSearchView/SearchBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import SwiftUI

struct SearchBar: View {
@Binding var text: String
var placeholder: String = "검색어를 입력하세요"
@FocusState.Binding var isFocused: Bool

var compact: Bool = false
var onSubmit: (() -> Void)?
var onMicTap: (() -> Void)?
var onClearTap: (() -> Void)?

var body: some View {
HStack(spacing: 8) {
searchIcon
textField
actionButton
}
.padding(.horizontal, compact ? 12 : 14)
.padding(.vertical, compact ? 10 : 12)
.background(searchBarBackground)
}
}

// MARK: - Components
private extension SearchBar {
var searchIcon: some View {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
}

var textField: some View {
TextField(placeholder, text: $text)
.focused($isFocused)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.submitLabel(.search)
.onSubmit { onSubmit?() }
}

var actionButton: some View {
Button {
if text.isEmpty { onMicTap?() } else { onClearTap?() }
} label: {
Image(systemName: text.isEmpty ? "mic.fill" : "xmark.circle.fill")
.font(.title3)
.foregroundStyle(.black)
.padding(6)
.animation(nil, value: text.isEmpty)
.frame(width: 44, height: 44)
}
.buttonStyle(.plain)
}

var searchBarBackground: some View {
RoundedRectangle(cornerRadius: compact ? 14 : 16)
.fill(Color.white)
.shadow(color: .black.opacity(0.05), radius: 6, x: 0, y: 2)
}
}
57 changes: 57 additions & 0 deletions BusRoad/Component/MainSearchView/WaveRingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import SwiftUI

struct WaveRingsView: View {
@State private var s1: CGFloat = 0.001
@State private var s2: CGFloat = 0.001
@State private var s3: CGFloat = 0.001

private let a1: CGFloat = 0.22
private let a2: CGFloat = 0.28
private let a3: CGFloat = 0.34

private let baseSize: CGFloat = 120
private let maxScale: CGFloat = 2.1
private let duration: Double = 2.0

var body: some View {
ZStack {
ring(scale: s1, baseAlpha: a1)
ring(scale: s2, baseAlpha: a2)
ring(scale: s3, baseAlpha: a3)
}
.frame(width: baseSize, height: baseSize)
.onAppear { start() }
.onDisappear { reset() }
}

private func ring(scale: CGFloat, baseAlpha: CGFloat) -> some View {
Circle()
.fill(Color.white.opacity(baseAlpha))
.scaleEffect(scale)
.opacity(max(0.0, min(1.0, (maxScale + 0.2) - scale)))
}

private func start() {
s1 = 0.001; s2 = 0.001; s3 = 0.001

withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
s1 = maxScale
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
s2 = maxScale
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.90) {
withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
s3 = maxScale
}
}
}

private func reset() {
withAnimation(.easeOut(duration: 0.2)) {
s1 = 0.001; s2 = 0.001; s3 = 0.001
}
}
}
7 changes: 0 additions & 7 deletions BusRoad/Component/dummyComponent.swift

This file was deleted.

32 changes: 32 additions & 0 deletions BusRoad/Feature/MainSearch/IntroSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

struct IntroSection: View {
@Binding var query: String
var isFocused: FocusState<Bool>.Binding

let onSubmit: () -> Void
let onMicTap: () -> Void
let onClear: () -> Void

var body: some View {
VStack(spacing: 16) {
Spacer()
Text("어디로 갈까요?")
.font(.system(size: 28, weight: .heavy))
.foregroundStyle(Color.green.opacity(0.9))

SearchBar(
text: $query,
placeholder: "장소 이름 검색하기",
isFocused: isFocused,
compact: false,
onSubmit: onSubmit,
onMicTap: onMicTap,
onClearTap: onClear
)
.padding(.horizontal, 16)

Spacer()
}
}
}
Loading