-
Notifications
You must be signed in to change notification settings - Fork 0
[feat/#466] TownSelectBottomSheet 구현 #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
1ed766a
679dea9
4ccb9c6
4f4a132
78277a8
2b113f5
f9a1ca5
d1d1fec
bf87746
05ac3e8
542c8bd
12466a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| // | ||
| // TownSelectBottomSheet.swift | ||
| // Solply | ||
| // | ||
| // Created by seozero on 3/19/26. | ||
| // | ||
|
|
||
| import SwiftUI | ||
|
|
||
| struct TownSelectBottomSheet: View { | ||
|
|
||
| // MARK: - Properties | ||
|
|
||
| @Environment(\.dismiss) private var dismiss | ||
|
|
||
| @State private var selectedTown: Town? | ||
| @State private var selectedSubTown: SubTown? | ||
|
|
||
| private let isTownLoading: Bool | ||
| private let townList: [Town] | ||
|
|
||
| private let initialTown: Town? | ||
| private let initialSubTown: SubTown? | ||
|
|
||
| private let onAppear: (() -> Void)? | ||
| private let onComplete: ((Town?, SubTown?) -> Void)? | ||
|
|
||
| init( | ||
| isTownLoading: Bool, | ||
| townList: [Town], | ||
| initialTown: Town? = nil, | ||
| initialSubTown: SubTown? = nil, | ||
| onAppear: (() -> Void)? = nil, | ||
| onComplete: ((Town?, SubTown?) -> Void)? = nil | ||
| ) { | ||
| self.isTownLoading = isTownLoading | ||
| self.townList = townList | ||
| self.initialTown = initialTown | ||
| self.initialSubTown = initialSubTown | ||
| self.onAppear = onAppear | ||
| self.onComplete = onComplete | ||
| } | ||
|
Comment on lines
+16
to
+42
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 init(
isTownLoading: Bool,
townList: [Town],
initialTown: Town,
initialSubTown: SubTown,
onAppear: (() -> Void? = nil,
onComplete: ((Town, SubTown) -> Void)? = nil
) {
self.isTownLoading = isTownLoading
self.townList = townList
self.onAppear = onAppear
self.onComplete = onComplete
self._selectedTown = State(initialValue: initialTown)
self._selectedSubTown = State(initialValue: initialSubTown)
}이렇게 초기값을 설정해주는 것은 어떤가요? |
||
|
|
||
|
Comment on lines
+13
to
+43
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 뷰는
MVI 패턴에 더 맞는 방향은,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예를 들어 지금 이 컴포에서 저 동네 리스트를 띄우기 위해 필요한 배열이라던가, '완료' 버튼을 누르면 실행할 action이라던가 등등.. 을 프로퍼티로 정의한 뒤에 부모뷰에서 초기화할 때 넘겨주는 방식으로 하면 결합도를 낮출 수 있을 거 같아요
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MVI 쓰면서 멀어진 것들 :
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 서브 뷰가 Store를 직접 가졌을 때, 우리가 의도한 단방향 데이터 흐름이 깨진다는 말이 이해가 갔습니다 ! |
||
| // MARK: - Body | ||
|
|
||
| var body: some View { | ||
| ZStack(alignment: .bottom) { | ||
| VStack(alignment: .center, spacing: 0) { | ||
| header | ||
|
|
||
| if !isTownLoading { | ||
| divider | ||
| } | ||
|
|
||
| HStack(alignment: .top, spacing: 0) { | ||
| townListView | ||
|
|
||
| Divider() | ||
|
|
||
| subTownListView | ||
| } | ||
| .customLoading(.JGDLoading, isLoading: isTownLoading) | ||
| } | ||
| .ignoresSafeArea(edges: .bottom) | ||
|
|
||
| SolplyMainButton( | ||
| title: "완료", | ||
| isEnabled: true | ||
| ) { | ||
| onComplete?(selectedTown, selectedSubTown) | ||
| } | ||
|
Comment on lines
+66
to
+71
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 별 건 아닌데, 어떤 작업 연결해야하는지 TODO 주석으로 남겨주세용 |
||
| .padding(.horizontal, 20.adjustedWidth) | ||
| .padding(.bottom, 38.adjustedHeight) | ||
| } | ||
| .onAppear { | ||
| onAppear?() | ||
| } | ||
| .onChange(of: townList) { | ||
| if selectedTown == nil { | ||
| selectedTown = initialTown | ||
| selectedSubTown = initialSubTown | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Subviews | ||
|
|
||
| extension TownSelectBottomSheet { | ||
| private var header: some View { | ||
| HStack(alignment: .center, spacing: 0) { | ||
| Text("동네") | ||
| .applySolplyFont(.title_18_sb) | ||
| .foregroundStyle(.coreBlack) | ||
|
|
||
| Spacer() | ||
|
|
||
| Button { | ||
| dismiss() | ||
| } label: { | ||
| Image(.xIconSm) | ||
| .resizable() | ||
| .aspectRatio(contentMode: .fit) | ||
| .frame(width: 24.adjusted, height: 24.adjusted) | ||
| .foregroundStyle(.gray800) | ||
|
Comment on lines
+101
to
+105
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Image(아이콘) 리사이저블 밑에 |
||
| } | ||
| .buttonStyle(.plain) | ||
| } | ||
| .padding(.horizontal, 16.adjustedWidth) | ||
| .padding(.top, 24.adjustedHeight) | ||
| .padding(.bottom, 20.adjustedHeight) | ||
| } | ||
|
|
||
| private var divider: some View { | ||
| Rectangle() | ||
| .frame(height: 1) | ||
| .foregroundStyle(.gray300) | ||
| } | ||
|
|
||
| private var townListView: some View { | ||
| VStack(alignment: .center, spacing: 0) { | ||
| ForEach(townList, id: \.self) { town in | ||
| JGDTopTownRow( | ||
| title: town.townName, | ||
| isSelected: selectedTown?.id == town.id | ||
| ) { | ||
| selectedTown = town | ||
| selectedSubTown = nil | ||
| } | ||
| } | ||
|
|
||
| Spacer() | ||
| } | ||
| .frame(maxHeight: .infinity) | ||
| .background(.gray100) | ||
| } | ||
|
|
||
| private var subTownListView: some View { | ||
| let subTowns = selectedTown?.subTowns ?? [] | ||
|
|
||
| return VStack(alignment: .center, spacing: 0) { | ||
| ForEach(subTowns, id: \.self) { subTown in | ||
| JGDSubTownRow( | ||
| title: subTown.townName, | ||
| isSelected: selectedSubTown?.id == subTown.id | ||
| ) { | ||
| selectedSubTown = subTown | ||
| } | ||
| } | ||
|
|
||
| Spacer() | ||
| } | ||
| .frame(width: 247.adjustedWidth) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // | ||
| // AIRecommendEffect.swift | ||
| // Solply | ||
| // | ||
| // Created by seozero on 3/20/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| struct AIRecommendEffect { | ||
| private let townService: TownAPI | ||
|
|
||
| init(townService: TownAPI) { | ||
| self.townService = townService | ||
| } | ||
|
|
||
| func fetchTowns() async -> AIRecommendPromptAction { | ||
| do { | ||
| let response = try await townService.fetchTownList() | ||
|
|
||
| guard let data = response.data else { | ||
| return .fetchTownsFailure(error: .responseError) | ||
| } | ||
|
|
||
| let towns = data.toEntity() | ||
|
|
||
| return .fetchTownsSuccess(townList: towns) | ||
| } catch let error as NetworkError { | ||
| return .fetchTownsFailure(error: error) | ||
| } catch { | ||
| return .fetchTownsFailure(error: .unknownError) | ||
| } | ||
| } | ||
| } |

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그리고
onComplete클로저에Town이랑SubTown가selectedTown,selectedSubTown때문에 옵셔널인 거 같은데, 얘도 옵셔널이 아니어도 괜찮을 거 같아요! 혹시 제가 놓친 부분이 있다면 알려주세용