Skip to content

[feat/#290] 장소 제보하기 UI 구현#296

Merged
SeungWon1125 merged 17 commits intodevelopfrom
feat/3290-place-request-ui
Oct 12, 2025
Merged

[feat/#290] 장소 제보하기 UI 구현#296
SeungWon1125 merged 17 commits intodevelopfrom
feat/3290-place-request-ui

Conversation

@SeungWon1125
Copy link
Copy Markdown
Collaborator

📄 작업 내용

  • 장소 제보하기 UI 구현했습니다.
  • 장소 검색 -> 장소 제보하기 navigation 연결했습니다.
구현 내용 iPhone 13 mini iPhone 16 pro
검색 결과 선택
직접 등록
태그 선택
나머지 기능, 로띠

💻 주요 코드 설명

이 뷰 분기처리..

저는 첨에 네비게이션인 줄 알았는데
모든 요소하 하나의 뷰에서 계속 분기처리를 해야하더라고요
그래서 뷰의 상태를 나타내는 RegisterStep enumState에 저장하고
해당 상태(단계)가 바뀔 때마다 각 섹션을 다르게 그리거나 EmptyView()로 표시하게 구현했어요

// RegisterStep.swift

enum RegisterStep {
    case searchPlace // 화면 진입 시 검색바 & 검색 결과가 나오는 단계
    case selectMainTagType // 메인 태그 선택하는 단계
    case selectExtraFeatures // 서브 태그 & 추가 정보 입력하는 단계
}

이렇게 단계를 enum으로 두고

// RegisterView.swift
// 메인 태그 선택하는 섹션

 Group {
    switch store.state.registerStep {
    case .searchPlace:
        EmptyView() // 검색 단계에는 이 뷰가 나올 필요가 없으니 EmptyView
    case .selectMainTagType, .selectExtraFeatures: // 이 두 단계에서 계속 표시되어야 함
        VStack(alignment: .leading, spacing: 12.adjustedHeight) {
            sectionTitle("장소 유형")
            
            SolplyDropDown(
                title: "장소 유형을 선택해주세요",
                tagOptions: Array(MainTagType.allCases.dropFirst()),
                selected: store.state.selectedMainTag
            ) { mainTag in
                store.dispatch(.selectMainTag(mainTag: mainTag))
                store.dispatch(.fetchSubTags(parentId: mainTag.parentId))
            }
            .padding(.horizontal, 16.adjustedWidth)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

예시로 메인 태그를 선택하는 섹션인데
switch문으로 UI가 표시되야아할 때, 표시되지 않아도 될 때를 구분해서 분기처리 했습니다

CustomFlowLayout 구현

그 앱잼때 주영이가 구현했던 ChipButtonsContainerView를 사용하려고 했는데
제 화면에서는 그 친구들이 위에 DropDown이 열리고 닫힘에 따라 같이 움직여야 하는데
GeometryReader기반이라서 애니메이션 적용이 안 되더라고요
(좌표 계산 타이밍 때문에 그렇다고 합니다)
=> 그래서 찾아보니 커스텀으로 Layout을 구현하는 방법이 있어서 해보았습니다.

// CustomFlowLayout.swift

struct CustomFlowLayout: Layout {
    private let horizontalSpacing: CGFloat
    private let verticalSpacing: CGFloat
    
    init(horizontalSpacing: CGFloat, verticalSpacing: CGFloat) {
        self.horizontalSpacing = horizontalSpacing
        self.verticalSpacing = verticalSpacing
    }

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard !subviews.isEmpty else { return .zero }

        let containerWidth = proposal.width ?? 0
        let lineHeight: CGFloat = 40.adjustedHeight
        var currentX: CGFloat = 0
        var lineCount: Int = 0

        for subview in subviews {
            let idealSize = subview.sizeThatFits(.unspecified)

            if currentX + idealSize.width > containerWidth && currentX != 0 {
                currentX = 0
                lineCount += 1
            }

            currentX += idealSize.width

            if subview != subviews.last {
                currentX += horizontalSpacing
            }
        }
        
        let totalLines = lineCount + 1
        
        let totalHeight = CGFloat(totalLines) * lineHeight + CGFloat(max(0, totalLines - 1)) * verticalSpacing

        return CGSize(width: containerWidth, height: totalHeight)
    }

    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) {
        let containerWidth = bounds.width
        let lineHeight: CGFloat = 40.adjustedHeight
        var currentX: CGFloat = bounds.minX
        var currentY: CGFloat = bounds.minY

        for subview in subviews {
            let idealSize = subview.sizeThatFits(.unspecified)

            if currentX + idealSize.width > containerWidth + horizontalSpacing && currentX != bounds.minX {
                currentX = bounds.minX
                currentY += lineHeight + verticalSpacing
            }

            subview.place(
                at: CGPoint(x: currentX, y: currentY),
                anchor: .topLeading,
                proposal: ProposedViewSize(width: idealSize.width, height: lineHeight)
            )

            currentX += idealSize.width

            if subview != subviews.last {
                currentX += horizontalSpacing
            }
        }
    }
}

코드가 좀 길긴 한데, 간단하게 설명하면 GeometryReader를 사용하지 않고
Swift의 Layout 프로토콜을 기반으로 작성했어요
Layout 프로토콜을 채택하면 아래 두 함수를 필수로 구현해야합니다.

sizeThatFits(proposal:subviews:cache:)
  • 레이아웃의 전체 크기를 계산하는 함수
  • 즉, 서브뷰들의 레이아웃에 따라 이 레이아웃의 전체 크기를 얼마만큼 차지해야하는지 계산합니다.
  • 약간 Swift한테 "이만큼 자리가 필요해~" 하는 느낌이라고 생각했어요
placeSubviews(in:proposal:subviews:cache:)
  • 계산된 크기 안에서 실제로 뷰들을 배치하는 함수
  • 위에 sizeThatFits()에서 계산한 크기 안에서 서브뷰들을 배치합니다
  • CurrentX, CurrentY를 기준으로 셀들을 하나하나 배치해요.

배치될 셀 너비를 계산하고 spacing값을 더한 값을 추가해 나가다가 CurrentX값이 최대 너비를 넘어가려고 하면
CurrentY를 다음 줄 위치로 증가시키고 CurrentX를 다시 0으로 초기화하는 방식으로 셀을 배치합니다

🔗 연결된 이슈

📚 참고자료

https://www.reddit.com/r/SwiftUI/comments/18pvz6g/recommended_swiftui_approach_to_creating_this/
https://developer.apple.com/documentation/swiftui/layout

👀 기타 더 이야기해볼 점

어제 솔커톤 고생했다 요플리들아~
좀 더 파이팅해봅시다

@SeungWon1125 SeungWon1125 requested a review from a team October 11, 2025 12:06
@SeungWon1125 SeungWon1125 self-assigned this Oct 11, 2025
@SeungWon1125 SeungWon1125 added 🦒 seungwon 승원이가함! 🛠️ feat 새로운 기능 구현 시 사용 labels Oct 11, 2025
@SeungWon1125 SeungWon1125 linked an issue Oct 11, 2025 that may be closed by this pull request
1 task
Copy link
Copy Markdown
Contributor

@pedro0527 pedro0527 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혼자 세상 많이 했던 ㄹㅇ 솔 커톤 수고했승wOn

Comment on lines +12 to +27
// MARK: - Properties

private let places: [PlaceSearchDTO]
private let placeDetailAction: ((Int, Int) -> Void)?
private let registerAction: (() -> Void)?

// MARK: - Initializer

init(
places: [PlaceSearchDTO] = []
places: [PlaceSearchDTO] = [],
placeDetailAction: ((Int, Int) -> Void)? = nil,
registerAction: (() -> Void)? = nil
) {
self.places = places
self.placeDetailAction = placeDetailAction
self.registerAction = registerAction
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goooood 수정 감사함다ㅎ

Comment on lines +42 to +43
PlaceDataView(places: store.state.places) { townId, placeId in
appCoordinator.navigate(to: .placeDetail(townId: townId, placeId: placeId))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이렇게 바꾼거구나....
이렇게 하면 원래 내가 했던 방식이랑 비교해서 더 좋은점? 그런게 있는건가여

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

화면 전환은 PlaceDataView가 하는 게 아니라 PlaceSearchView가 하는 게 맞다고 생각헀어요 그래서 Subview까지 AppCoordinator를 들고 있을 필요가 없다고 생각하여 이렇게 수정했어요!

Comment on lines +35 to +40
extension RegisterCompleteView {
func waitForLottie() async {
try? await Task.sleep(nanoseconds: 2_000_000_000)
appCoordinator.goToRoot()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로띠 약간 왼쪽으로 가있는거 어케 해결해...?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나만 그렇게 느끼던 거 아니었구나.. Preview에서 영역 잡아가면서 확인했는데, 중앙이긴 하거든요..? 근데 기분탓인진 모르겠는데 왼쪽으로 좀 치우친 느낌... 오늘 qa받고 이상하다 하면 수정해 볼게용

Comment on lines +79 to +81
.onChange(of: text) { _, newValue in
onChange?(text)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나 그 장소 검색할 때 생각해보다가 결국 엔터키나 검색 아이콘을 눌렀을때 검색 기능을 하는건데 굳이 change가 필요할까? 싶어서 지웠는데 꼭 필요할까??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그건 맞는데, onSubmit이 사용자가 키보드의 enter를 누를 때 불린다고 해서
사용자가 그냥 화면을 눌러서 키보드를 내릴 경우를 대비해 onChange를 두긴 했어요
(onSubmit은 사용자가 키보드의 return키를 누르지 않으면 호출되지 않는다고 합니다)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 아직 5개 제한 안해놓은거지?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 그때 장소 검색에서도 빈값 넣고 검색하면 화면 안바뀌고 그대로 놔뒀는데 그거 처리 안한거지?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 아직 안 해놨어요

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 나 앱잼때도 난 엔티티 안쓰고 하긴 했는데....혹시 무조건 엔티티를 써야하는건가여?
내가 설명해줬는데 기억 못하는거면 미안..😢

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그때는 시간이 부족해서 그래도 DTO를 가져다 사용헀지만, DTOEntity는 계층이 다르다고 생각하면 됩니다!
사실 클린아키의 일부를 채택한 느낌..
DTO는 서버랑 통신하기 위한 객체이고, Entity는 순수 데이터 모델이라 네트워크 계층에서는 DTO만 알면 되고, Presentation에서는 Entity만 알고 있으면 돼요
나중에 DTO에 필드명이 바뀌거나 그러면 DTO -> Entity로 변환하는 그 코드만 수정하면 되겠죠?
반면 DTO를 사용하면 Service나, Reducer에서도 코드를 다 수정해야 합니다. 즉, 전체 계층에 수정사항이 생기기 때문에 이를 방지하고자 Entity로 변환하여 사용하는 것도 있어요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 같이 클아 공부하면서 이유를 더 찾아봅시다!

Comment on lines +37 to +40
Rectangle()
.frame(maxWidth: .infinity)
.frame(height: 1.adjustedHeight)
.foregroundStyle(.gray200)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

divider 말고 이 방법도 있군 good

@SeungWon1125 SeungWon1125 merged commit 61efb7b into develop Oct 12, 2025
@SeungWon1125 SeungWon1125 deleted the feat/3290-place-request-ui branch October 12, 2025 06:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠️ feat 새로운 기능 구현 시 사용 🦒 seungwon 승원이가함!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 장소 등록하기 UI 구현

2 participants