Skip to content

Commit df694e6

Browse files
authored
Merge pull request #10 from DeveloperAcademy-POSTECH/feat/#2/destination-search
Feat/#2/destination search
2 parents a873984 + 8a3ff0e commit df694e6

24 files changed

+1152
-54
lines changed

.DS_Store

-6 KB
Binary file not shown.

BusRoad.xcodeproj/project.pbxproj

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
objects = {
88

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

1314
/* Begin PBXFileReference section */
15+
60043BAA2E8AC91300167A28 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Secrets.plist; sourceTree = "<group>"; };
1416
60A8CDA12E7CDFE500530B92 /* BusRoad.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BusRoad.app; sourceTree = BUILT_PRODUCTS_DIR; };
1517
60A8CDAF2E7CE7A100530B92 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
1618
/* End PBXFileReference section */
@@ -51,6 +53,7 @@
5153
60A8CD982E7CDFE500530B92 = {
5254
isa = PBXGroup;
5355
children = (
56+
60043BAA2E8AC91300167A28 /* Secrets.plist */,
5457
60A8CDAF2E7CE7A100530B92 /* .swiftlint.yml */,
5558
60A8CDA32E7CDFE500530B92 /* BusRoad */,
5659
60A8CDA22E7CDFE500530B92 /* Products */,
@@ -75,7 +78,7 @@
7578
60A8CD9D2E7CDFE500530B92 /* Sources */,
7679
60A8CD9E2E7CDFE500530B92 /* Frameworks */,
7780
60A8CD9F2E7CDFE500530B92 /* Resources */,
78-
60A8CDB12E7CE82300530B92 /* ShellScript */,
81+
60A8CDB12E7CE82300530B92 /* Run Script */,
7982
);
8083
buildRules = (
8184
);
@@ -130,14 +133,15 @@
130133
isa = PBXResourcesBuildPhase;
131134
buildActionMask = 2147483647;
132135
files = (
136+
60043BAB2E8AC91300167A28 /* Secrets.plist in Resources */,
133137
60A8CDB02E7CE7B900530B92 /* .swiftlint.yml in Resources */,
134138
);
135139
runOnlyForDeploymentPostprocessing = 0;
136140
};
137141
/* End PBXResourcesBuildPhase section */
138142

139143
/* Begin PBXShellScriptBuildPhase section */
140-
60A8CDB12E7CE82300530B92 /* ShellScript */ = {
144+
60A8CDB12E7CE82300530B92 /* Run Script */ = {
141145
isa = PBXShellScriptBuildPhase;
142146
alwaysOutOfDate = 1;
143147
buildActionMask = 2147483647;
@@ -148,6 +152,7 @@
148152
inputPaths = (
149153
"$(SRCROOT)/.swiftlint.yml",
150154
);
155+
name = "Run Script";
151156
outputFileListPaths = (
152157
);
153158
outputPaths = (
@@ -304,6 +309,8 @@
304309
ENABLE_USER_SCRIPT_SANDBOXING = NO;
305310
GENERATE_INFOPLIST_FILE = YES;
306311
INFOPLIST_FILE = BusRoad/Info.plist;
312+
INFOPLIST_KEY_NSMicrophoneUsageDescription = "\"음성 검색 기능을 사용하기 위해 마이크 접근 권한이 필요합니다.\"";
313+
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "\"말씀하신 내용을 텍스트로 변환하기 위해 음성 인식 권한이 필요합니다.\"";
307314
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
308315
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
309316
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -342,6 +349,8 @@
342349
ENABLE_USER_SCRIPT_SANDBOXING = NO;
343350
GENERATE_INFOPLIST_FILE = YES;
344351
INFOPLIST_FILE = BusRoad/Info.plist;
352+
INFOPLIST_KEY_NSMicrophoneUsageDescription = "\"음성 검색 기능을 사용하기 위해 마이크 접근 권한이 필요합니다.\"";
353+
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "\"말씀하신 내용을 텍스트로 변환하기 위해 음성 인식 권한이 필요합니다.\"";
345354
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
346355
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
347356
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import SwiftUI
2+
3+
struct PlaceCard: View {
4+
let title: String
5+
let address: String
6+
var searchQuery: String?
7+
var onTap: (() -> Void)? // 카드 탭 액션
8+
9+
var body: some View {
10+
Button(action: { onTap?() }) {
11+
VStack(alignment: .leading, spacing: 6) {
12+
13+
if let query = searchQuery, !query.isEmpty {
14+
Text(title.highlightedText(searchQuery: query))
15+
.lineLimit(1)
16+
.truncationMode(.tail)
17+
} else {
18+
Text(title)
19+
.font(.headline)
20+
.foregroundStyle(.primary)
21+
.lineLimit(1)
22+
.truncationMode(.tail)
23+
}
24+
25+
Text(address)
26+
.font(.subheadline)
27+
.foregroundStyle(.secondary)
28+
.lineLimit(2)
29+
.truncationMode(.tail)
30+
}
31+
.frame(maxWidth: .infinity, alignment: .leading)
32+
.padding(14)
33+
.background(
34+
RoundedRectangle(cornerRadius: 12)
35+
.fill(Color(.white))
36+
)
37+
.overlay(
38+
RoundedRectangle(cornerRadius: 12)
39+
.stroke(Color(.separator), lineWidth: 0.5)
40+
)
41+
.shadow(color: Color.black.opacity(0.04), radius: 3, x: 0, y: 1)
42+
}
43+
.buttonStyle(.plain)
44+
.contentShape(RoundedRectangle(cornerRadius: 12))
45+
.accessibilityElement(children: .ignore)
46+
.accessibilityLabel("\(title), \(address)")
47+
}
48+
}
49+
50+
// MARK: - 텍스트 하이라이트 헬퍼
51+
extension String {
52+
/// 검색어와 일치하는 부분을 찾아서 AttributedString으로 변환
53+
func highlightedText(searchQuery: String, highlightColor: Color = .green) -> AttributedString {
54+
var attributedString = AttributedString(self)
55+
56+
guard !searchQuery.isEmpty else { return attributedString }
57+
58+
let lowercasedText = self.lowercased()
59+
let lowercasedQuery = searchQuery.lowercased()
60+
61+
var searchStartIndex = lowercasedText.startIndex
62+
63+
while let range = lowercasedText.range(of: lowercasedQuery, range: searchStartIndex..<lowercasedText.endIndex) {
64+
// AttributedString의 범위로 변환
65+
if let attributedRange = Range(range, in: attributedString) {
66+
// 하이라이트 색상 및 굵기 적용
67+
attributedString[attributedRange].foregroundColor = highlightColor
68+
attributedString[attributedRange].font = .system(.headline, design: .default, weight: .bold)
69+
}
70+
71+
// 다음 검색을 위해 시작 인덱스 업데이트
72+
searchStartIndex = range.upperBound
73+
}
74+
75+
return attributedString
76+
}
77+
}
78+
79+
#Preview {
80+
VStack(spacing: 12) {
81+
PlaceCard(
82+
title: "포항 영일대해수욕장",
83+
address: "경북 포항시 북구 두호동 685",
84+
searchQuery: "포항"
85+
)
86+
PlaceCard(
87+
title: "테라로사 포스텍점",
88+
address: "포항시 남구 청암로 87",
89+
searchQuery: "포항"
90+
)
91+
PlaceCard(
92+
title: "일반 카드 (하이라이트 없음)",
93+
address: "일반 주소"
94+
)
95+
}
96+
.padding()
97+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import SwiftUI
2+
3+
struct SearchBar: View {
4+
@Binding var text: String
5+
var placeholder: String = "검색어를 입력하세요"
6+
@FocusState.Binding var isFocused: Bool
7+
8+
var compact: Bool = false
9+
var onSubmit: (() -> Void)?
10+
var onMicTap: (() -> Void)?
11+
var onClearTap: (() -> Void)?
12+
13+
var body: some View {
14+
HStack(spacing: 8) {
15+
searchIcon
16+
textField
17+
actionButton
18+
}
19+
.padding(.horizontal, compact ? 12 : 14)
20+
.padding(.vertical, compact ? 10 : 12)
21+
.background(searchBarBackground)
22+
}
23+
}
24+
25+
// MARK: - Components
26+
private extension SearchBar {
27+
var searchIcon: some View {
28+
Image(systemName: "magnifyingglass")
29+
.foregroundStyle(.secondary)
30+
}
31+
32+
var textField: some View {
33+
TextField(placeholder, text: $text)
34+
.focused($isFocused)
35+
.textInputAutocapitalization(.never)
36+
.disableAutocorrection(true)
37+
.submitLabel(.search)
38+
.onSubmit { onSubmit?() }
39+
}
40+
41+
var actionButton: some View {
42+
Button {
43+
if text.isEmpty { onMicTap?() } else { onClearTap?() }
44+
} label: {
45+
Image(systemName: text.isEmpty ? "mic.fill" : "xmark.circle.fill")
46+
.font(.title3)
47+
.foregroundStyle(.black)
48+
.padding(6)
49+
.animation(nil, value: text.isEmpty)
50+
.frame(width: 44, height: 44)
51+
}
52+
.buttonStyle(.plain)
53+
}
54+
55+
var searchBarBackground: some View {
56+
RoundedRectangle(cornerRadius: compact ? 14 : 16)
57+
.fill(Color.white)
58+
.shadow(color: .black.opacity(0.05), radius: 6, x: 0, y: 2)
59+
}
60+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import SwiftUI
2+
3+
struct WaveRingsView: View {
4+
@State private var s1: CGFloat = 0.001
5+
@State private var s2: CGFloat = 0.001
6+
@State private var s3: CGFloat = 0.001
7+
8+
private let a1: CGFloat = 0.22
9+
private let a2: CGFloat = 0.28
10+
private let a3: CGFloat = 0.34
11+
12+
private let baseSize: CGFloat = 120
13+
private let maxScale: CGFloat = 2.1
14+
private let duration: Double = 2.0
15+
16+
var body: some View {
17+
ZStack {
18+
ring(scale: s1, baseAlpha: a1)
19+
ring(scale: s2, baseAlpha: a2)
20+
ring(scale: s3, baseAlpha: a3)
21+
}
22+
.frame(width: baseSize, height: baseSize)
23+
.onAppear { start() }
24+
.onDisappear { reset() }
25+
}
26+
27+
private func ring(scale: CGFloat, baseAlpha: CGFloat) -> some View {
28+
Circle()
29+
.fill(Color.white.opacity(baseAlpha))
30+
.scaleEffect(scale)
31+
.opacity(max(0.0, min(1.0, (maxScale + 0.2) - scale)))
32+
}
33+
34+
private func start() {
35+
s1 = 0.001; s2 = 0.001; s3 = 0.001
36+
37+
withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
38+
s1 = maxScale
39+
}
40+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
41+
withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
42+
s2 = maxScale
43+
}
44+
}
45+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.90) {
46+
withAnimation(.easeOut(duration: duration).repeatForever(autoreverses: false)) {
47+
s3 = maxScale
48+
}
49+
}
50+
}
51+
52+
private func reset() {
53+
withAnimation(.easeOut(duration: 0.2)) {
54+
s1 = 0.001; s2 = 0.001; s3 = 0.001
55+
}
56+
}
57+
}

BusRoad/Component/dummyComponent.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import SwiftUI
2+
3+
struct IntroSection: View {
4+
@Binding var query: String
5+
var isFocused: FocusState<Bool>.Binding
6+
7+
let onSubmit: () -> Void
8+
let onMicTap: () -> Void
9+
let onClear: () -> Void
10+
11+
var body: some View {
12+
VStack(spacing: 16) {
13+
Spacer()
14+
Text("어디로 갈까요?")
15+
.font(.system(size: 28, weight: .heavy))
16+
.foregroundStyle(Color.green.opacity(0.9))
17+
18+
SearchBar(
19+
text: $query,
20+
placeholder: "장소 이름 검색하기",
21+
isFocused: isFocused,
22+
compact: false,
23+
onSubmit: onSubmit,
24+
onMicTap: onMicTap,
25+
onClearTap: onClear
26+
)
27+
.padding(.horizontal, 16)
28+
29+
Spacer()
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)