Skip to content
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

[WEAV-74] 프로필 입력 - 회사 선택 #29

Merged
merged 8 commits into from
Oct 10, 2024
Merged
4 changes: 3 additions & 1 deletion .github/workflows/unitTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ jobs:
run: |
curl https://mise.run | sh
mise install

- name: Create Secret.swift file
run: |
echo '${{ secrets.SECRET_SWIFT }}' > ./Projects/Core/CoreKit/Sources/Secret.swift
- name: Install Tuist dependencies
run: mise x -- tuist install

Expand Down
2 changes: 2 additions & 0 deletions Projects/App/Sources/Navigation/NavigationStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ extension PathType {
AuthProfileGenderInputView()
case .authProfileAge:
AuthProfileAgeInputView()
case .authCompany:
AuthCompanyView()
case .authName:
AuthNameInputView()
}
Expand Down
22 changes: 21 additions & 1 deletion Projects/Core/CommonKit/Sources/Path/PathTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ public enum PathType: Hashable {
public static var debugPreviewTypes: [PathType] = [
.designPreview,
.main,
.signUp(.authPhoneInput),
.signUp(
.authPhoneVerify(
SMSSendResponse(
userType: .NEW,
authCodeId: "tempCode",
phoneNumber: "010-1234-5678"
)
)
),
.signUp(.authAgreement),
.signUp(.authGreeting),
.signUp(.authProfileGender),
.signUp(.authProfileAge),
.signUp(.authCompany),
.signUp(.authName),
.signUp(.authPhoneInput)
]
#endif
Expand All @@ -35,6 +51,7 @@ public enum PathType: Hashable {
case .authGreeting: return "가입 후 환영"
case .authProfileGender: return "성별 입력"
case .authProfileAge: return "나이 입력"
case .authCompany: return "내 직장 입력"
case .authName: return "이름 입력"
}
}
Expand All @@ -49,6 +66,7 @@ public enum SignUpSubViewType: Hashable {
case authGreeting
case authProfileGender
case authProfileAge
case authCompany
case authName

public static func == (lhs: SignUpSubViewType, rhs: SignUpSubViewType) -> Bool {
Expand All @@ -69,8 +87,10 @@ public enum SignUpSubViewType: Hashable {
hasher.combine(4)
case .authProfileAge:
hasher.combine(5)
case .authName:
case .authCompany:
hasher.combine(6)
case .authName:
hasher.combine(7)
}
}
}
4 changes: 2 additions & 2 deletions Projects/Core/CoreKit/UnitTest/StringExtensionText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ final class PhoneNumberTests: XCTestCase {
"010123456789".isValidPhoneNumber(),
"유효하지 않은 번호 형식임에도 불구하고 true가 반환됨"
)
XCTAssertFalse(
XCTAssertTrue(
"010-1234-5678".isValidPhoneNumber(),
"하이픈이 포함된 경우 유효하지 않은 번호 형식임에도 불구하고 true가 반환됨"
"하이픈이 포함되었으나 유효한 번호인 경우 true를 반환해야 함"
)

// 텍스트가 포함된 경우
Expand Down
19 changes: 19 additions & 0 deletions Projects/Core/Model/Sources/Network/CompanySearchResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// CompanySearchResponse.swift
// CommonKit
//
// Created by 김지수 on 10/9/24.
// Copyright © 2024 com.weave. All rights reserved.
//

import Foundation

public struct CompanySearchResponse {
public let id: String
public let name: String

public init(id: String, name: String) {
self.id = id
self.name = name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// CompanyService.swift
// CommonKit
//
// Created by 김지수 on 10/9/24.
// Copyright © 2024 com.weave. All rights reserved.
//

import Foundation
import OpenapiGenerated
import Model

//MARK: - Service Protocol
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

// MARK: 주석 형식을 수정해야 합니다.

SwiftLint에 따르면, 주석('//') 뒤에 최소한 하나의 공백이 필요하며, 'MARK' 주석은 '// MARK: ...' 또는 '// MARK: - ...' 형식을 따라야 합니다.

다음과 같이 수정할 수 있습니다:

-//MARK: - Service Protocol
+// MARK: - Service Protocol

-//MARK: - Service
+// MARK: - Service

Also applies to: 21-21

🧰 Tools
🪛 SwiftLint

[Warning] 13-13: Prefer at least one space after slashes for comments

(comment_spacing)


[Warning] 13-13: MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...'

(mark)

public protocol CompanyServiceProtocol {
func requestSearchCompany(
keyword: String,
next: String?
) async throws -> ([CompanySearchResponse], String?)
}

//MARK: - Service
public final class CompanyService {
public static var shared = CompanyService()
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

싱글톤 인스턴스를 'let'으로 선언하는 것이 좋습니다.

싱글톤 패턴에서는 인스턴스가 변경되지 않으므로 'var' 대신 'let'을 사용하여 불변성을 보장하는 것이 좋습니다.

다음과 같이 수정할 수 있습니다:

-public static var shared = CompanyService()
+public static let shared = CompanyService()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static var shared = CompanyService()
public static let shared = CompanyService()

private init() {}
}

extension CompanyService: CompanyServiceProtocol {
public func requestSearchCompany(
keyword: String,
next: String? = nil
) async throws -> ([CompanySearchResponse], String?) {
let response = try await client.searchCompanies(
query: .init(name: keyword, next: next)
).ok.body.json
Comment on lines +32 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

'client' 인스턴스가 정의되지 않았습니다.

'client'를 사용하여 searchCompanies 메서드를 호출하고 있는데, 현재 클래스 내에서 'client'가 정의되어 있지 않습니다. 'client'를 선언하고 초기화해야 합니다.


let result = response.companies.map {
CompanySearchResponse(
id: $0.id,
name: $0.name
)
}
let next = response.next
return (result, next)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// CompanyServiceMock.swift
// CommonKit
//
// Created by 김지수 on 10/10/24.
// Copyright © 2024 com.weave. All rights reserved.
//

import Foundation
import Model

//MARK: - Service
public final class CompanyServiceMock: CompanyServiceProtocol {

public init() {}
Comment on lines +13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

초기화 메서드를 개선하여 더 유연한 테스트를 가능하게 할 수 있습니다.

현재 초기화 메서드는 매개변수가 없어 고정된 동작만 가능합니다. 다음과 같이 개선하여 다양한 테스트 시나리오를 지원할 수 있습니다:

public init(companies: [Model.CompanySearchResponse] = [], delay: TimeInterval = 0, shouldSimulateError: Bool = false) {
    self.companies = companies
    self.delay = delay
    self.shouldSimulateError = shouldSimulateError
}

이렇게 하면 테스트 시 다양한 상황을 시뮬레이션할 수 있습니다.


public func requestSearchCompany(keyword: String, next: String?) async throws -> ([Model.CompanySearchResponse], String?) {
return ([
.init(id: "0", name: "현대글로비스"),
.init(id: "1", name: "현대자동차"),
.init(id: "2", name: "기아자동차"),
.init(id: "3", name: "채널톡"),
.init(id: "4", name: "닥터다이어리"),
.init(id: "5", name: "컬쳐커넥션"),
.init(id: "6", name: "엄청좋은회사"),
.init(id: "7", name: "로지텍"),
.init(id: "8", name: "스탠리"),
.init(id: "9", name: "스타벅스"),
.init(id: "10", name: "호날두주식회사"),
.init(id: "11", name: "애플"),
.init(id: "12", name: "엔비디아"),
], nil)
}
Comment on lines +17 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

requestSearchCompany 메서드의 구현을 개선해야 합니다.

현재 구현은 실제 검색 동작을 정확히 시뮬레이션하지 않습니다. 다음과 같은 개선사항을 제안합니다:

  1. keyword 매개변수를 사용하여 회사 목록을 필터링하세요.
  2. next 매개변수를 활용하여 페이지네이션을 시뮬레이션하세요.
  3. 네트워크 지연이나 오류 상황을 시뮬레이션할 수 있도록 구현을 확장하세요.

예시 구현:

public func requestSearchCompany(keyword: String, next: String?) async throws -> ([Model.CompanySearchResponse], String?) {
    if shouldSimulateError {
        throw NSError(domain: "CompanyServiceMock", code: 0, userInfo: [NSLocalizedDescriptionKey: "Simulated network error"])
    }

    if delay > 0 {
        try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
    }

    let filteredCompanies = companies.filter { $0.name.lowercased().contains(keyword.lowercased()) }
    let pageSize = 10
    let startIndex = Int(next ?? "0") ?? 0
    let endIndex = min(startIndex + pageSize, filteredCompanies.count)
    let nextPage = endIndex < filteredCompanies.count ? String(endIndex) : nil

    return (Array(filteredCompanies[startIndex..<endIndex]), nextPage)
}

이 구현은 더 현실적인 검색 동작을 제공하며, 다양한 테스트 시나리오를 지원합니다.

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ actor LoggingMiddleware {
private let logger: Logger
package let bodyLoggingPolicy: BodyLoggingPolicy

package init(logger: Logger = defaultLogger, bodyLoggingConfiguration: BodyLoggingPolicy = .upTo(maxBytes: 2048)) {
package init(logger: Logger = defaultLogger, bodyLoggingConfiguration: BodyLoggingPolicy = .upTo(maxBytes: 10000)) {
self.logger = logger
self.bodyLoggingPolicy = bodyLoggingConfiguration
}
Expand Down Expand Up @@ -77,19 +77,17 @@ extension LoggingMiddleware: ServerMiddleware {

extension LoggingMiddleware {
func log(_ request: HTTPRequest, _ requestBody: BodyLoggingPolicy.BodyLog) {
logger.debug(
"Request: \(request.method, privacy: .public) \(request.path ?? "<nil>", privacy: .public) body: \(requestBody, privacy: .auto)"
)
let decodedPath = request.path?.removingPercentEncoding ?? "<nil>"
print("Request: \(request.method) \(decodedPath) body: \(requestBody)")
}

func log(_ request: HTTPRequest, _ response: HTTPResponse, _ responseBody: BodyLoggingPolicy.BodyLog) {
logger.debug(
"Response: \(request.method, privacy: .public) \(request.path ?? "<nil>", privacy: .public) \(response.status, privacy: .public) body: \(responseBody, privacy: .auto)"
)
let decodedPath = request.path?.removingPercentEncoding ?? "<nil>"
print("Response: \(request.method) \(decodedPath) \(response.status) body: \(responseBody)")
}

func log(_ request: HTTPRequest, failedWith error: any Error) {
logger.warning("Request failed. Error: \(error.localizedDescription)")
print("Request failed. Error: \(error.localizedDescription)")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ import SwiftUI
public struct CTAButton<BackgroundStyle: ShapeStyle>: View {
private let title: String
private let backgroundStyle: BackgroundStyle
private let titleColor: Color = .white
private let titleColor: Color
private let isActive: Bool
private var handler: () -> Void

public init(
title: String,
titleColor: Color = .white,
backgroundStyle: BackgroundStyle = DesignCore.Colors.grey500,
isActive: Bool = true,
handler: @escaping () -> Void
) {
self.title = title
self.titleColor = titleColor
self.backgroundStyle = backgroundStyle
self.isActive = isActive
self.handler = handler
Expand Down
123 changes: 123 additions & 0 deletions Projects/DesignSystem/DesignCore/Sources/DropDown/DropDownView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// DropDownView.swift
// DesignCore
//
// Created by 김지수 on 10/9/24.
// Copyright © 2024 com.weave. All rights reserved.
//

import SwiftUI

public protocol DropDownFetchable: Hashable, Equatable {
var id: String { get }
var name: String { get }
}

public struct DropDownPicker<Content: View>: View {

@FocusState var showDropDown: Bool
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

'FocusState' 대신 'Binding' 사용 필요

'FocusState'는 주로 키보드 포커스 관리에 사용되며, 현재 상황에서는 'Binding'을 사용하는 것이 적절합니다. 외부에서 전달된 'FocusState'를 내부에서 프로퍼티 래퍼 변수에 할당하는 것은 컴파일 오류를 유발할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

- @FocusState var showDropDown: Bool
+ @Binding var showDropDown: Bool

...

- showDropDown: FocusState<Bool>,
+ showDropDown: Binding<Bool>,

...

- self._showDropDown = showDropDown
+ self._showDropDown = showDropDown

...

Also applies to: 52-52, 59-59


var tapHandler: ((Int) -> Void)?
var content: () -> Content

var dataSources: [any DropDownFetchable]
let itemSize: CGFloat = 56

var nextPageHandler: (() -> Void)?
var needCallNextPage = false

var frameHeight: CGFloat {
if !showDropDown {
return 0
}

if dataSources.count > 3 {
return itemSize * 3 + itemSize * 0.5
}

return itemSize * CGFloat(dataSources.count)
}

var frameOffset: CGFloat {
if !showDropDown {
return 0
}

if dataSources.count > 3 {
return 140
}

return 125 - 28 * CGFloat(3 - dataSources.count)
}

public init(
dataSources: [any DropDownFetchable],
showDropDown: FocusState<Bool>,
needCallNextPage: Bool = false,
@ViewBuilder content: @escaping () -> Content,
tapHandler: ((Int) -> Void)? = nil,
nextPageHandler: (() -> Void)? = nil
) {
self.dataSources = dataSources
self.content = content
self.tapHandler = tapHandler
self._showDropDown = showDropDown
self.nextPageHandler = nextPageHandler
self.needCallNextPage = needCallNextPage
}

public var body: some View {
VStack {
ZStack {
ZStack {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(.white) //
Comment on lines +73 to +74
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

'foregroundStyle(.white)' 대신 'fill(Color.white)' 사용 권장

'RoundedRectangle'에 색상을 적용할 때는 'foregroundStyle'보다 'fill'을 사용하는 것이 적절합니다.

다음과 같이 수정하는 것을 제안합니다:

RoundedRectangle(cornerRadius: 24)
-     .foregroundStyle(.white)
+     .fill(Color.white)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(.white) //
RoundedRectangle(cornerRadius: 24)
.fill(Color.white)

ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< dataSources.count, id: \.self) { index in
let item = dataSources[index]
Button(action: {
tapHandler?(index)
withAnimation {
showDropDown.toggle()
}
}, label: {
HStack(spacing: 16) {
Text(item.name)
.typography(.regular_14)
.multilineTextAlignment(.leading)
Spacer()
}
.foregroundStyle(DesignCore.Colors.grey500)
.frame(height: itemSize)
.padding(.horizontal, 16)
.background(.white)
})
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if needCallNextPage {
ProgressView()
.padding(.bottom, 10)
.onAppear {
nextPageHandler?()
}
}
}
}
.clipShape(
RoundedRectangle(cornerRadius: 24)

)
.shadow(.default)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

'shadow(.default)' 사용 오류 수정 필요

'shadow' 모디파이어에 '.default'를 사용하는 것은 컴파일 오류를 발생시킬 수 있습니다. 대신 적절한 형식의 'shadow'를 사용하세요.

다음과 같이 수정하는 것을 제안합니다:

- .shadow(.default)
+ .shadow(radius: 4)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.shadow(.default)
.shadow(radius: 4)

.animation(.easeInOut(duration: 0.2), value: frameOffset)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

'.animation()' 모디파이어의 사용을 지양하세요

SwiftUI에서 .animation() 모디파이어는 iOS 15 이상에서 더 이상 권장되지 않습니다. 대신 애니메이션이 필요한 프로퍼티에 직접 애니메이션을 적용하거나, 상태 변화 시 withAnimation 블록을 활용하는 것이 좋습니다.

다음과 같이 수정하는 것을 제안합니다:

-     .animation(.easeInOut(duration: 0.2), value: frameOffset)

필요한 곳에서 적절한 애니메이션을 적용해 주세요.

Committable suggestion was skipped due to low confidence.

.frame(height: frameHeight)
.offset(y: frameOffset)

content()
}
.frame(height: 50)
}
.zIndex(999)
}
}
Loading
Loading