diff --git a/src/Projects/BKDesign/PreviewApp/Info.plist b/src/Projects/BKDesign/PreviewApp/Info.plist
new file mode 100644
index 00000000..912bf849
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Info.plist
@@ -0,0 +1,68 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+
+
+ UILaunchStoryboardName
+ LaunchScreen
+
+
+ UIDeviceFamily
+
+ 1
+
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ Default Configuration
+ UISceneDelegateClassName
+ BKDesignPreviewApp.SceneDelegate
+
+
+
+
+
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
+ UIAppFonts
+
+ Pretendard-Regular.otf
+ Pretendard-Medium.otf
+ Pretendard-SemiBold.otf
+ Pretendard-Bold.otf
+
+
+
diff --git a/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard b/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard
new file mode 100644
index 00000000..dd79351e
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Resources/LaunchScreen.storyboard
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift b/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift
new file mode 100644
index 00000000..d7723cda
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Sources/AppDelegate.swift
@@ -0,0 +1,23 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return true
+ }
+
+ // MARK: UISceneSession Lifecycle
+
+ func application(
+ _ application: UIApplication,
+ configurationForConnecting connectingSceneSession: UISceneSession,
+ options: UIScene.ConnectionOptions
+ ) -> UISceneConfiguration {
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
+ }
+}
diff --git a/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift b/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift
new file mode 100644
index 00000000..29e6c422
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Sources/SceneDelegate.swift
@@ -0,0 +1,24 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import BKDesign
+import UIKit
+
+class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+ var window: UIWindow?
+
+ func scene(
+ _ scene: UIScene,
+ willConnectTo session: UISceneSession,
+ options connectionOptions: UIScene.ConnectionOptions
+ ) {
+ guard let windowScene = scene as? UIWindowScene else { return }
+
+ let window = UIWindow(windowScene: windowScene)
+ let viewController = BKButtonTestViewController()
+// let viewController = BKButtonGroupDemoViewController()
+ window.rootViewController = UINavigationController(rootViewController: viewController)
+ window.makeKeyAndVisible()
+
+ self.window = window
+ }
+}
diff --git a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift
new file mode 100644
index 00000000..f31b030f
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonGroupDemoViewController.swift
@@ -0,0 +1,105 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import BKDesign
+import SnapKit
+import UIKit
+
+public final class BKButtonGroupDemoViewController: UIViewController {
+
+ private let scrollView = UIScrollView()
+ private let containerView = UIStackView()
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .white
+ setupScrollView()
+ setupDemoGroups()
+ }
+
+ private func setupScrollView() {
+ view.addSubview(scrollView)
+ scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
+
+ scrollView.addSubview(containerView)
+ containerView.axis = .vertical
+ containerView.spacing = 16
+ containerView.alignment = .fill
+ containerView.distribution = .fill
+ containerView.snp.makeConstraints {
+ $0.edges.equalToSuperview().inset(20)
+ $0.width.equalToSuperview().inset(20)
+ }
+ }
+
+ private func setupDemoGroups() {
+ addSection(title: "Custom ๊ตฌ์ฑ (์ง์ ๋ง๋ ๊ทธ๋ฃน)")
+ containerView.addArrangedSubview(BKButtonGroup(
+ buttons: [
+ makeButton(title: "[Custom] S", size: .small),
+ makeButton(title: "[Custom] M", size: .medium),
+ makeButton(title: "[Custom] L", size: .large)
+ ],
+ layout: .horizontal
+ ))
+
+ addDivider()
+
+ addSection(title: "TwoButtonGroup")
+ containerView.addArrangedSubview(BKButtonGroup.twoButtonGroup(
+ leftTitle: "์ทจ์", rightTitle: "ํ์ธ",
+ leftAction: { print("์ทจ์ tapped") },
+ rightAction: { print("ํ์ธ tapped") }
+ ))
+
+ addDivider()
+
+ addSection(title: "ThreeButtonGroup")
+ containerView.addArrangedSubview(BKButtonGroup.threeButtonGroup(
+ leftTitle: "์ด์ ", centerTitle: "์ค๊ฐ", rightTitle: "๋ค์",
+ leftAction: { print("์ด์ tapped") },
+ centerAction: { print("์ค๊ฐ tapped") },
+ rightAction: { print("๋ค์ tapped") }
+ ))
+
+ addDivider()
+
+ addSection(title: "SingleFullButton")
+ containerView.addArrangedSubview(BKButtonGroup.singleFullButton(
+ title: "๊ณ์ํ๊ธฐ",
+ action: { print("๊ณ์ํ๊ธฐ tapped") }
+ ))
+
+ addDivider()
+
+ addSection(title: "VerticalGroup (Rounded ๋ฒํผ)")
+ containerView.addArrangedSubview(BKButtonGroup.verticalButtonGroup(buttons: [
+ makeButton(title: "๋ฅ๊ธ1", size: .rounded),
+ makeButton(title: "๋ฅ๊ธ2", size: .rounded)
+ ]))
+ }
+
+ private func makeButton(title: String, size: BKButtonSize) -> BKButton {
+ let button = BKButton.primary(title: title, size: size)
+ button.addAction(UIAction { _ in
+ print("Tapped: \(title)")
+ }, for: .touchUpInside)
+ return button
+ }
+
+ private func addSection(title: String) {
+ let label = UILabel()
+ label.text = title
+ label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
+ label.textColor = .darkGray
+ containerView.addArrangedSubview(label)
+ }
+
+ private func addDivider() {
+ let divider = UIView()
+ divider.backgroundColor = UIColor.lightGray.withAlphaComponent(0.4)
+ divider.snp.makeConstraints { make in
+ make.height.equalTo(1)
+ }
+ containerView.addArrangedSubview(divider)
+ }
+}
diff --git a/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift
new file mode 100644
index 00000000..83f4ee84
--- /dev/null
+++ b/src/Projects/BKDesign/PreviewApp/Sources/View/BKButtonTestViewController.swift
@@ -0,0 +1,133 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import BKDesign
+import SnapKit
+import UIKit
+
+public final class BKButtonTestViewController: UIViewController {
+
+ // MARK: - UI Components
+ private let scrollView = UIScrollView()
+ private let containerView = UIStackView()
+
+ // MARK: - Lifecycle
+
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .white
+ setupScrollView()
+// setupIndependentButtons()
+ setupStackView()
+ }
+
+ public override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ setupAllTestButtons()
+ }
+
+ // MARK: - Setup Scroll & Stack
+
+ private func setupScrollView() {
+ view.addSubview(scrollView)
+ scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
+
+ scrollView.addSubview(containerView)
+ containerView.snp.makeConstraints {
+ $0.edges.equalToSuperview().inset(20)
+ $0.width.equalToSuperview().inset(20)
+ }
+ }
+
+ private func setupStackView() {
+ containerView.axis = .vertical
+ containerView.spacing = 16
+ containerView.alignment = .fill
+ containerView.distribution = .equalSpacing
+ }
+
+ // MARK: - Setup Buttons by Size
+
+ private func setupAllTestButtons() {
+ setupButtons(for: .large)
+ setupButtons(for: .medium)
+ setupButtons(for: .small)
+ setupButtons(for: .rounded)
+ }
+
+ private func setupButtons(for size: BKButtonSize) {
+ addButton("Primary", style: .primary, size: size)
+ addButton("Secondary", style: .secondary, size: size)
+ addButton("Tertiary", style: .tertiary, size: size)
+
+ addIconButton("Apple ๋ก๊ทธ์ธ", style: .primary, size: size, left: .appleLogo)
+ addIconButton("์นด์นด์ค ๋ก๊ทธ์ธ", style: .primary, size: size, right: .kakaoLogo)
+ addIconButton("์์ชฝ ์์ด์ฝ", style: .primary, size: size, left: .appleLogo, right: .kakaoLogo)
+ }
+
+ // MARK: - Button Builders
+
+ private func addButton(_ title: String, style: BKButtonStyle, size: BKButtonSize) {
+ let button = BKButton(style: style, size: size)
+ button.title = "[\(size.label)] \(title)"
+ containerView.addArrangedSubview(button)
+ }
+
+ private func addIconButton(
+ _ title: String,
+ style: BKButtonStyle,
+ size: BKButtonSize,
+ left: BKIcon? = nil,
+ right: BKIcon? = nil
+ ) {
+ let button = BKButton(style: style, size: size)
+ button.title = "[\(size.label)] \(title)"
+ button.leftIcon = left?.image
+ button.rightIcon = right?.image
+ containerView.addArrangedSubview(button)
+ }
+
+ private func setupIndependentButtons() {
+ let sampleView = UIView()
+ scrollView.addSubview(sampleView)
+ sampleView.snp.makeConstraints {
+ $0.top.equalTo(containerView.snp.bottom).offset(40)
+ $0.centerX.equalToSuperview()
+ $0.bottom.lessThanOrEqualToSuperview()
+ }
+
+ let buttons: [BKButton] = [
+ .primary(title: "[Free] Apple ๋ก๊ทธ์ธ", size: .large),
+ .secondary(title: "[Free] Secondary", size: .large),
+ .tertiary(title: "[Free] Tertiary", size: .large),
+ .primary(title: "[Free] Apple ๋ก๊ทธ์ธ", size: .medium),
+ .secondary(title: "[Free] Secondary", size: .small),
+ .tertiary(title: "[Free] Tertiary", size: .rounded)
+ ]
+
+ buttons[0].leftIcon = BKIcon.appleLogo.image
+ buttons[2].rightIcon = BKIcon.kakaoLogo.image
+
+ var last: UIView?
+ for button in buttons {
+ sampleView.addSubview(button)
+ button.snp.makeConstraints {
+ $0.centerX.equalToSuperview()
+ $0.top.equalTo(last?.snp.bottom ?? sampleView.snp.top).offset(16)
+ }
+ last = button
+ }
+ }
+
+}
+
+
+public extension BKButtonSize {
+ var label: String {
+ switch self {
+ case .large: return "Large"
+ case .medium: return "Medium"
+ case .small: return "Small"
+ case .rounded: return "Rounded"
+ }
+ }
+}
diff --git a/src/Projects/BKDesign/Project.swift b/src/Projects/BKDesign/Project.swift
index 90fd3d14..9a6de982 100644
--- a/src/Projects/BKDesign/Project.swift
+++ b/src/Projects/BKDesign/Project.swift
@@ -14,7 +14,8 @@ let project = Project.project(
swiftLintScript
],
dependencies: [
- .core()
+ .core(),
+ .external(dependency: .SnapKit)
]
),
Target.target(
@@ -25,6 +26,35 @@ let project = Project.project(
dependencies: [
.design()
]
+ ),
+ Target.target(
+ name: "BKDesignPreviewApp",
+ product: .app,
+ bundleId: "designpreview." + Project.bundleID,
+ infoPlist: .file(path: .relativeToRoot("Projects/BKDesign/PreviewApp/Info.plist")), // ๋ณต์ฌ๋ณธ!
+ sources: ["PreviewApp/Sources/**"],
+ resources: [
+ "PreviewApp/Resources/**",
+ .glob(pattern: .relativeToRoot("Projects/BKDesign/Resources/Font/**"))
+ ],
+ scripts: [
+ swiftLintScript
+ ],
+ dependencies: [
+ .design() // BKDesign ๋ชจ๋ ์์กด์ฑ
+ ],
+ settings: .settings(
+ base: [
+ "DEVELOPMENT_LANGUAGE": "ko",
+ "CODE_SIGN_STYLE": "Automatic" // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ฑ์ ์๋์ผ๋ก
+ ],
+ configurations: [
+ .debug(name: "Debug", xcconfig: .relativeToRoot("SupportingFiles/Booket/Debug.xcconfig")),
+ ]
+ )
)
+
+
]
)
+
diff --git a/src/Projects/BKDesign/Sources/Components/BKButton.swift b/src/Projects/BKDesign/Sources/Components/BKButton.swift
deleted file mode 100644
index 55807205..00000000
--- a/src/Projects/BKDesign/Sources/Components/BKButton.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-// Copyright ยฉ 2025 Booket. All rights reserved
-
-import Foundation
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift
new file mode 100644
index 00000000..fe453e76
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButton.swift
@@ -0,0 +1,373 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import SnapKit
+import UIKit
+
+public class BKButton: UIButton, BKButtonProtocol {
+
+ @SetNeeds(.layout, .display)
+ public var style: BKButtonStyle = .primary {
+ didSet {
+ updateButtonStyle()
+ }
+ }
+
+ @SetNeeds(.layout)
+ public var size: BKButtonSize = .medium {
+ didSet {
+ updateButtonSize()
+ }
+ }
+
+ @SetNeeds(wrappedValue: nil, .layout)
+ public var title: String? {
+ didSet {
+ setTitle("", for: .normal) // UIButton ๊ธฐ๋ณธ ํ์ดํ ์ ๊ฑฐ
+ customTitleLabel.text = title
+ updateLayout()
+ }
+ }
+
+ @SetNeeds(wrappedValue: nil, .layout)
+ public var leftIcon: UIImage? {
+ didSet {
+ updateLeftIcon()
+ updateLayout()
+ }
+ }
+
+ @SetNeeds(wrappedValue: nil, .layout)
+ public var rightIcon: UIImage? {
+ didSet {
+ updateRightIcon()
+ updateLayout()
+ }
+ }
+
+ @SetNeeds(.layout, .display)
+ public var isDisabled: Bool = false {
+ didSet {
+ isEnabled = !isDisabled
+ updateButtonState()
+ }
+ }
+
+ public var isFullWidth: Bool = false {
+ didSet {
+ invalidateIntrinsicContentSize()
+ }
+ }
+
+ // MARK: - Override UIButton Properties
+ public override var isHighlighted: Bool {
+ didSet {
+ updateButtonState()
+ animatePressedState()
+ }
+ }
+
+ public override var isEnabled: Bool {
+ didSet {
+ updateButtonState()
+ }
+ }
+
+ // MARK: - Private Properties
+ // ์ปค์คํ
๋ ์ด์์์ ์ํ ๋ทฐ
+ private let customContainerView = UIView()
+ private let leftIconView = UIImageView()
+ private let customTitleLabel = UILabel()
+ private let rightIconView = UIImageView()
+ private let stackView = UIStackView()
+
+ // Animation properties
+ private let pressedScale: CGFloat = 0.96
+ private let animationDuration: TimeInterval = 0.15
+ private let animationSpringDamping: CGFloat = 0.7
+ private let animationSpringVelocity: CGFloat = 0.5
+
+ private var currentState: BKButtonState {
+ return BKButtonState(isEnabled: isEnabled, isHighlighted: isHighlighted)
+ }
+
+ // MARK: - Initialization
+ public init(style: BKButtonStyle = .primary, size: BKButtonSize = .medium) {
+ super.init(frame: .zero)
+ self.style = style
+ self.size = size
+ setupButton()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupButton()
+ }
+
+ private func setupButton() {
+ _style.configure(with: self)
+ _size.configure(with: self)
+ _title.configure(with: self)
+ _leftIcon.configure(with: self)
+ _rightIcon.configure(with: self)
+ _isDisabled.configure(with: self)
+
+ // UIButton์ ๊ธฐ๋ณธ ์์๋ค์ ์จ๊น
+ setTitle("", for: .normal)
+ setImage(nil, for: .normal)
+
+ // iOS 15.0+ Configuration ๋นํ์ฑํ
+ if #available(iOS 15.0, *) {
+ configuration = nil
+ }
+
+ setupCustomViews()
+ updateButtonStyle()
+ updateButtonSize()
+ updateLayout()
+ }
+
+ private func setupCustomViews() {
+ addSubview(customContainerView)
+ customContainerView.snp.makeConstraints { make in
+ make.edges.equalToSuperview()
+ }
+
+ customContainerView.isUserInteractionEnabled = false
+
+ customContainerView.addSubview(stackView)
+ stackView.axis = .horizontal
+ stackView.alignment = .center
+ stackView.distribution = .equalCentering
+ stackView.isUserInteractionEnabled = false
+
+ stackView.addArrangedSubview(leftIconView)
+ stackView.addArrangedSubview(customTitleLabel)
+ stackView.addArrangedSubview(rightIconView)
+
+ stackView.snp.makeConstraints { make in
+ make.centerX.equalToSuperview()
+ make.left.greaterThanOrEqualToSuperview().offset(size.horizontalPadding)
+ make.right.lessThanOrEqualToSuperview().offset(-size.horizontalPadding)
+
+ make.centerY.equalToSuperview()
+ make.top.greaterThanOrEqualToSuperview().offset(size.verticalPadding)
+ make.bottom.lessThanOrEqualToSuperview().offset(-size.verticalPadding)
+ }
+
+ setupIconViews()
+ setupTitleLabel()
+ }
+
+ private func setupIconViews() {
+ [leftIconView, rightIconView].forEach { iconView in
+ iconView.contentMode = .scaleAspectFit
+ iconView.isHidden = true
+ iconView.isUserInteractionEnabled = false
+ iconView.setContentHuggingPriority(.required, for: .horizontal)
+ iconView.setContentCompressionResistancePriority(.required, for: .horizontal)
+ }
+
+ leftIconView.snp.makeConstraints { make in
+ make.width.height.equalTo(size.iconSize.width)
+ }
+
+ rightIconView.snp.makeConstraints { make in
+ make.width.height.equalTo(size.iconSize.width)
+ }
+ }
+
+ private func setupTitleLabel() {
+ customTitleLabel.textAlignment = .center
+ customTitleLabel.numberOfLines = 1
+ customTitleLabel.isUserInteractionEnabled = false
+ customTitleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ customTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
+ }
+
+ // MARK: - Update Methods
+ private func updateButtonState() {
+ updateColors()
+ }
+
+ private func updateButtonStyle() {
+ updateColors()
+ }
+
+ private func updateButtonSize() {
+ customTitleLabel.font = size.font
+ updateCornerRadius()
+ updateIconSizes()
+ invalidateIntrinsicContentSize()
+ }
+
+ private func updateColors() {
+ let backgroundColors = style.backgroundColors
+ let foregroundColors = style.foregroundColors
+
+ backgroundColor = backgroundColors.color(for: currentState)
+ let foregroundColor = foregroundColors.color(for: currentState)
+
+ customTitleLabel.textColor = foregroundColor
+ leftIconView.tintColor = foregroundColor
+ rightIconView.tintColor = foregroundColor
+ }
+
+ private func updateCornerRadius() {
+ if size != .rounded {
+ layer.cornerRadius = size.cornerRadius
+ }
+ layer.masksToBounds = true
+ }
+
+ private func updateIconSizes() {
+ let iconSize = size.iconSize.width
+ leftIconView.snp.updateConstraints { make in
+ make.width.height.equalTo(iconSize)
+ }
+ rightIconView.snp.updateConstraints { make in
+ make.width.height.equalTo(iconSize)
+ }
+ }
+
+ private func updateLayout() {
+ stackView.spacing = 0
+ stackView.setCustomSpacing(0, after: leftIconView)
+ stackView.setCustomSpacing(0, after: customTitleLabel)
+
+ if leftIcon != nil, let title = title, !title.isEmpty {
+ stackView.setCustomSpacing(size.iconSpacing, after: leftIconView)
+ }
+
+ if rightIcon != nil, let title = title, !title.isEmpty {
+ stackView.setCustomSpacing(size.iconSpacing, after: customTitleLabel)
+ }
+ }
+
+ private func updateLeftIcon() {
+ if let icon = leftIcon {
+ let resizedIcon = icon.resize(to: size.iconSize)
+ leftIconView.image = resizedIcon?.withRenderingMode(.alwaysTemplate)
+ leftIconView.isHidden = false
+ } else {
+ leftIconView.image = nil
+ leftIconView.isHidden = true
+ }
+ }
+
+ private func updateRightIcon() {
+ if let icon = rightIcon {
+ let resizedIcon = icon.resize(to: size.iconSize)
+ rightIconView.image = resizedIcon?.withRenderingMode(.alwaysTemplate)
+ rightIconView.isHidden = false
+ } else {
+ rightIconView.image = nil
+ rightIconView.isHidden = true
+ }
+ }
+
+ // MARK: - Animation
+ private func animatePressedState() {
+ if isHighlighted {
+ UIView.animate(
+ withDuration: animationDuration,
+ delay: 0,
+ usingSpringWithDamping: animationSpringDamping,
+ initialSpringVelocity: animationSpringVelocity,
+ options: [.allowUserInteraction, .beginFromCurrentState],
+ animations: {
+ self.transform = CGAffineTransform(scaleX: self.pressedScale, y: self.pressedScale)
+ }
+ )
+ } else {
+ UIView.animate(
+ withDuration: animationDuration,
+ delay: 0,
+ usingSpringWithDamping: animationSpringDamping,
+ initialSpringVelocity: animationSpringVelocity,
+ options: [.allowUserInteraction, .beginFromCurrentState],
+ animations: {
+ self.transform = .identity
+ }
+ )
+ }
+ }
+
+
+ // MARK: - Layout
+ override public func layoutSubviews() {
+ super.layoutSubviews()
+
+ // 'Rounded'์ผ ๋๋ง ๋์ ์ผ๋ก ๋ฐ์ง๋ฆ ์ค์
+ if size == .rounded {
+ let height = bounds.height
+ let width = bounds.width
+
+ let minimumRadius = height / 2
+ let dynamicRadius = width / 2
+
+ layer.cornerRadius = min(minimumRadius, dynamicRadius)
+ }
+
+ }
+
+ public override var intrinsicContentSize: CGSize {
+ let stackSize = stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
+
+ let minimumTotalPadding = size.horizontalPadding * 2
+
+ let currentButtonWidth = bounds.width
+ let calculatedPadding = max((currentButtonWidth - stackSize.width), minimumTotalPadding)
+
+ let intrinsicWidth = stackSize.width + calculatedPadding
+
+ if isFullWidth {
+ return CGSize(width: UIView.noIntrinsicMetric, height: size.height)
+ }
+
+ return CGSize(width: intrinsicWidth, height: size.height)
+ }
+
+}
+
+// MARK: - Factory Methods
+extension BKButton {
+ public static func primary(
+ title: String,
+ size: BKButtonSize = .medium
+ ) -> BKButton {
+ let button = BKButton(style: .primary, size: size)
+ button.title = title
+ return button
+ }
+
+ public static func secondary(
+ title: String,
+ size: BKButtonSize = .medium
+ ) -> BKButton {
+ let button = BKButton(style: .secondary, size: size)
+ button.title = title
+ return button
+ }
+
+ public static func tertiary(
+ title: String,
+ size: BKButtonSize = .medium
+ ) -> BKButton {
+ let button = BKButton(style: .tertiary, size: size)
+ button.title = title
+ return button
+ }
+}
+
+// MARK: - Configuration Support
+extension BKButton {
+ public func configure(with configuration: BKButtonConfiguration) {
+ style = configuration.style
+ size = configuration.size
+ title = configuration.title
+ leftIcon = configuration.leftIcon
+ rightIcon = configuration.rightIcon
+ isDisabled = !configuration.isEnabled
+ isFullWidth = configuration.isFullWidth
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift
new file mode 100644
index 00000000..0782b259
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonConfiguration.swift
@@ -0,0 +1,110 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+/// `BKButton`์ ์ ์ฉํ ์ธ๋ถ ๊ตฌ์ฑ ์ ๋ณด๋ฅผ ๋ด๋ ๊ตฌ์กฐ์ฒด์
๋๋ค.
+///
+/// ๋ฒํผ ์คํ์ผ, ํฌ๊ธฐ, ํ์ดํ, ์์ด์ฝ, ์ฌ์ฉ ๊ฐ๋ฅ ์ํ ๋ฑ์ ํตํฉ ๊ด๋ฆฌํ๋ฉฐ
+/// ์ค์ ์ ์ฒด์ด๋ ๋ฐฉ์์ผ๋ก ์์ฝ๊ฒ ์กฐํฉํ ์ ์์ต๋๋ค.
+public struct BKButtonConfiguration {
+
+ /// ๋ฒํผ ์คํ์ผ (๊ธฐ๋ณธ๊ฐ: `.primary`)
+ var style: BKButtonStyle
+
+ /// ๋ฒํผ ์ฌ์ด์ฆ (๊ธฐ๋ณธ๊ฐ: `.medium`)
+ var size: BKButtonSize
+
+ /// ๋ฒํผ ํ์ดํ ํ
์คํธ
+ var title: String?
+
+ /// ์ผ์ชฝ์ ํ์ํ ์์ด์ฝ
+ var leftIcon: UIImage?
+
+ /// ์ค๋ฅธ์ชฝ์ ํ์ํ ์์ด์ฝ
+ var rightIcon: UIImage?
+
+ /// ๋ฒํผ ํ์ฑํ ์ํ (๊ธฐ๋ณธ๊ฐ: `true`)
+ var isEnabled: Bool
+
+ /// ๊ฐ๋ก๋ก ๊ฝ ์ฐจ๋์ง ์ฌ๋ถ (๊ธฐ๋ณธ๊ฐ: `false`)
+ var isFullWidth: Bool
+
+ /// ์ด๊ธฐํ ๋ฉ์๋
+ ///
+ /// - Parameters:
+ /// - style: ๋ฒํผ ์คํ์ผ
+ /// - size: ๋ฒํผ ํฌ๊ธฐ
+ /// - title: ๋ฒํผ ํ
์คํธ
+ /// - leftIcon: ์ข์ธก ์์ด์ฝ ์ด๋ฏธ์ง
+ /// - rightIcon: ์ฐ์ธก ์์ด์ฝ ์ด๋ฏธ์ง
+ /// - isEnabled: ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
+ /// - isFullWidth: ๊ฐ๋ก ๊ฝ ์ฑ์ฐ๊ธฐ ์ฌ๋ถ
+ public init(
+ style: BKButtonStyle = .primary,
+ size: BKButtonSize = .medium,
+ title: String? = nil,
+ leftIcon: UIImage? = nil,
+ rightIcon: UIImage? = nil,
+ isEnabled: Bool = true,
+ isFullWidth: Bool = false
+ ) {
+ self.style = style
+ self.size = size
+ self.title = title
+ self.leftIcon = leftIcon
+ self.rightIcon = rightIcon
+ self.isEnabled = isEnabled
+ self.isFullWidth = isFullWidth
+ }
+
+ // MARK: - Immutable Modifier Helpers
+
+ /// ์คํ์ผ์ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withStyle(_ style: BKButtonStyle) -> BKButtonConfiguration {
+ var config = self
+ config.style = style
+ return config
+ }
+
+ /// ์ฌ์ด์ฆ๋ฅผ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withSize(_ size: BKButtonSize) -> BKButtonConfiguration {
+ var config = self
+ config.size = size
+ return config
+ }
+
+ /// ํ์ดํ์ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withTitle(_ title: String?) -> BKButtonConfiguration {
+ var config = self
+ config.title = title
+ return config
+ }
+
+ /// ์ผ์ชฝ ์์ด์ฝ์ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withLeftIcon(_ icon: UIImage?) -> BKButtonConfiguration {
+ var config = self
+ config.leftIcon = icon
+ return config
+ }
+
+ /// ์ค๋ฅธ์ชฝ ์์ด์ฝ์ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withRightIcon(_ icon: UIImage?) -> BKButtonConfiguration {
+ var config = self
+ config.rightIcon = icon
+ return config
+ }
+
+ /// ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withEnabled(_ enabled: Bool) -> BKButtonConfiguration {
+ var config = self
+ config.isEnabled = enabled
+ return config
+ }
+
+ /// ๊ฐ๋ก ์ฑ์ฐ๊ธฐ ์ฌ๋ถ๋ฅผ ๋ณ๊ฒฝํ ์๋ก์ด ์ค์ ๋ฐํ
+ public func withFullWidth(_ fullWidth: Bool) -> BKButtonConfiguration {
+ var config = self
+ config.isFullWidth = fullWidth
+ return config
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift
new file mode 100644
index 00000000..d717c544
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonGroup.swift
@@ -0,0 +1,198 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import SnapKit
+import UIKit
+
+public class BKButtonGroup: UIView {
+
+ // MARK: - Types
+ public enum Layout {
+ case horizontal
+ case vertical
+ case fullWidth
+ }
+
+ // MARK: - Properties
+ private let stackView = UIStackView()
+ private var buttons: [BKButton] = []
+
+ public var layout: Layout = .horizontal {
+ didSet {
+ updateLayout()
+ }
+ }
+
+ public var spacing: CGFloat = BKSpacing.spacing2 {
+ didSet {
+ stackView.spacing = spacing
+ }
+ }
+
+ // MARK: - Initialization
+
+ public init(
+ buttons: [BKButton],
+ layout: Layout = .horizontal,
+ spacing: CGFloat = BKSpacing.spacing2
+ ) {
+ self.buttons = buttons
+ self.layout = layout
+ self.spacing = spacing
+ super.init(frame: .zero)
+ setupView()
+ }
+
+ public required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupView()
+ }
+
+ // MARK: - Setup
+ private func setupView() {
+ addSubview(stackView)
+ stackView.snp.makeConstraints { make in
+ make.edges.equalToSuperview()
+ }
+
+ updateLayout()
+ updateButtons()
+ }
+
+ // MARK: - Public Methods
+ public func setButtons(_ buttons: [BKButton]) {
+ self.buttons = buttons
+ updateButtons()
+ }
+
+ public func addButton(_ button: BKButton) {
+ buttons.append(button)
+ updateButtons()
+ }
+
+ public func removeButton(_ button: BKButton) {
+ if let index = buttons.firstIndex(of: button) {
+ buttons.remove(at: index)
+ updateButtons()
+ }
+ }
+
+ public func removeAllButtons() {
+ buttons.removeAll()
+ updateButtons()
+ }
+
+ // MARK: - Private Methods
+ private func updateLayout() {
+ switch layout {
+ case .horizontal:
+ stackView.axis = .horizontal
+ stackView.distribution = .fillProportionally
+ stackView.alignment = .fill
+ buttons.forEach { $0.isFullWidth = false }
+
+ case .vertical:
+ stackView.axis = .vertical
+ stackView.distribution = .fill
+ stackView.alignment = .fill
+ buttons.forEach { $0.isFullWidth = true }
+
+ case .fullWidth:
+ stackView.axis = .horizontal
+ stackView.distribution = .fillEqually
+ stackView.alignment = .fill
+ buttons.forEach { $0.isFullWidth = true }
+ }
+
+ stackView.spacing = spacing
+ }
+
+ private func updateButtons() {
+ stackView.arrangedSubviews.forEach { view in
+ stackView.removeArrangedSubview(view)
+ view.removeFromSuperview()
+ }
+
+ buttons.forEach { button in
+ button.setContentHuggingPriority(.required, for: .horizontal)
+ stackView.addArrangedSubview(button)
+ }
+
+ updateLayout()
+ }
+
+}
+
+// MARK: - Convenience Initializers
+extension BKButtonGroup {
+ public static func twoButtonGroup(
+ leftTitle: String = "ํ์ธ",
+ rightTitle: String = "์ทจ์",
+ leftAction: (() -> Void)? = nil,
+ rightAction: (() -> Void)? = nil
+ ) -> BKButtonGroup {
+ let leftButton = BKButton.secondary(title: leftTitle)
+ let rightButton = BKButton.primary(title: rightTitle)
+
+ if let action = leftAction {
+ leftButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ if let action = rightAction {
+ rightButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ return BKButtonGroup(buttons: [leftButton, rightButton], layout: .fullWidth)
+ }
+
+ public static func singleFullButton(
+ title: String = "๋ค์",
+ action: (() -> Void)? = nil
+ ) -> BKButtonGroup {
+ let nextButton = BKButton.primary(title: title, size: .large)
+
+ if let action = action {
+ nextButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ return BKButtonGroup(buttons: [nextButton], layout: .vertical)
+ }
+
+ /// 3๊ฐ ๋ฒํผ ์ํ ๊ทธ๋ฃน
+ public static func threeButtonGroup(
+ leftTitle: String,
+ centerTitle: String,
+ rightTitle: String,
+ leftAction: (() -> Void)? = nil,
+ centerAction: (() -> Void)? = nil,
+ rightAction: (() -> Void)? = nil
+ ) -> BKButtonGroup {
+ let leftButton = BKButton.tertiary(title: leftTitle)
+ let centerButton = BKButton.secondary(title: centerTitle)
+ let rightButton = BKButton.primary(title: rightTitle)
+
+ if let action = leftAction {
+ leftButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ if let action = centerAction {
+ centerButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ if let action = rightAction {
+ rightButton.addAction(UIAction { _ in action() }, for: .touchUpInside)
+ }
+
+ return BKButtonGroup(buttons: [leftButton, centerButton, rightButton], layout: .horizontal)
+ }
+
+ /// ์์ง ๋ฒํผ ๊ทธ๋ฃน
+ ///
+ /// - Warning: ์
๋ ฅ๋ ๋ฒํผ๋ค์ isFullWidth ์์ฑ์ด true๋ก ๋ณ๊ฒฝ๋ฉ๋๋ค.
+ public static func verticalButtonGroup(
+ buttons: [BKButton],
+ spacing: CGFloat = BKSpacing.spacing2
+ ) -> BKButtonGroup {
+ buttons.forEach { $0.isFullWidth = true }
+ return BKButtonGroup(buttons: buttons, layout: .vertical, spacing: spacing)
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift
new file mode 100644
index 00000000..5bc3fff5
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonProtocol.swift
@@ -0,0 +1,34 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+/// BKButton ์คํ์ผ ๋ฒํผ์ ๊ตฌ์ฑํ๊ธฐ ์ํ ๊ณตํต ์ธํฐํ์ด์ค์
๋๋ค.
+///
+/// ์ด ํ๋กํ ์ฝ์ ๋ฒํผ์ ์ํ, ์คํ์ผ, ํฌ๊ธฐ, ํ
์คํธ ๋ฐ ์์ด์ฝ ์์ฑ์ ์ ์ํ์ฌ
+/// ์ผ๊ด๋ UI ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ ์ ์๋๋ก ๋์์ค๋๋ค.
+protocol BKButtonProtocol: AnyObject {
+
+ /// ๋ฒํผ์ ํ์ฑํ ์ฌ๋ถ๋ฅผ ๋ํ๋
๋๋ค.
+ ///
+ /// `true`์ธ ๊ฒฝ์ฐ ๋ฒํผ์ ๋นํ์ฑํ ์ํ์ด๋ฉฐ, ์ฌ์ฉ์์์ ์ํธ์์ฉ์ด ์ฐจ๋จ๋ฉ๋๋ค.
+ var isDisabled: Bool { get set }
+
+ /// ๋ฒํผ์ ์คํ์ผ์ ์ง์ ํฉ๋๋ค.
+ ///
+ /// ์คํ์ผ์ ๋ฐ๋ผ ๋ฐฐ๊ฒฝ์, ํ
์คํธ ์์ ๋ฑ์ด ๋ฌ๋ผ์ง๋๋ค.
+ var style: BKButtonStyle { get set }
+
+ /// ๋ฒํผ์ ํฌ๊ธฐ๋ฅผ ์ง์ ํฉ๋๋ค.
+ ///
+ /// ํฌ๊ธฐ์ ๋ฐ๋ผ ๋์ด, ํฐํธ, ํจ๋ฉ ๊ฐ ๋ฑ์ด ์กฐ์ ๋ฉ๋๋ค.
+ var size: BKButtonSize { get set }
+
+ /// ๋ฒํผ์ ํ์๋ ํ
์คํธ์
๋๋ค.
+ var title: String? { get set }
+
+ /// ๋ฒํผ์ ์ผ์ชฝ์ ํ์๋ ์์ด์ฝ ์ด๋ฏธ์ง์
๋๋ค.
+ var leftIcon: UIImage? { get set }
+
+ /// ๋ฒํผ์ ์ค๋ฅธ์ชฝ์ ํ์๋ ์์ด์ฝ ์ด๋ฏธ์ง์
๋๋ค.
+ var rightIcon: UIImage? { get set }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift
new file mode 100644
index 00000000..f6f00ad7
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonSize.swift
@@ -0,0 +1,121 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+/// `BKButton`์ ํฌ๊ธฐ ํ์
์ ์ ์ํ๋ ์ด๊ฑฐํ์
๋๋ค.
+///
+/// ๋ฒํผ์ ๋์ด, ํจ๋ฉ, ํฐํธ, ์์ด์ฝ ํฌ๊ธฐ, ๋ชจ์๋ฆฌ ๋ฐ๊ฒฝ ๋ฑ ๋ ์ด์์ ๊ด๋ จ ์์ฑ์ ์ํฅ์ ์ค๋๋ค.
+public enum BKButtonSize {
+ /// ์์ ๋ฒํผ
+ case small
+
+ /// ์ค๊ฐ ํฌ๊ธฐ ๋ฒํผ
+ case medium
+
+ /// ๋ํ ๋ฒํผ
+ case large
+
+ /// ๋ฅ๊ทผ ๋ฒํผ
+ case rounded
+
+ /// ๋ฒํผ ๋์ด ๊ฐ
+ var height: CGFloat {
+ switch self {
+ case .small, .rounded:
+ 40
+ case .medium:
+ 48
+ case .large:
+ 52
+ }
+ }
+
+ /// ๋ฒํผ ์ข์ฐ ํจ๋ฉ ๊ฐ
+ var horizontalPadding: CGFloat {
+ switch self {
+ case .small, .rounded:
+ BKSpacing.spacing3
+ case .medium:
+ BKSpacing.spacing4
+ case .large:
+ BKSpacing.spacing5
+ }
+ }
+
+ /// ๋ฒํผ ์ํ ํจ๋ฉ ๊ฐ
+ var verticalPadding: CGFloat {
+ switch self {
+ case .small, .rounded:
+ BKSpacing.spacing2
+ case .medium, .large:
+ BKSpacing.spacing3
+ }
+ }
+
+ /// ๋ฒํผ ํ
์คํธ์ ์ฌ์ฉํ ํฐํธ
+ var font: UIFont {
+ switch self {
+ case .rounded, .small, .medium:
+ BKTextStyle
+ .label1(weight: .medium).uiFont ??
+ .systemFont(
+ ofSize: BKTextStyle.label1(weight: .medium).fontAttributes.fontSize.rawValue,
+ weight: .medium
+ )
+ case .large:
+ BKTextStyle
+ .body1(weight: .medium).uiFont ??
+ .systemFont(
+ ofSize: BKTextStyle.body1(weight: .medium).fontAttributes.fontSize.rawValue,
+ weight: .medium
+ )
+ }
+ }
+
+ /// ์์ด์ฝ์ ํฌ๊ธฐ
+ var iconSize: CGSize {
+ switch self {
+ case .large:
+ CGSize(width: 24, height: 24)
+ default:
+ CGSize(width: 22, height: 22)
+ }
+ }
+
+ /// ์์ด์ฝ๊ณผ ํ
์คํธ ์ฌ์ด์ ๊ฐ๊ฒฉ
+ var iconSpacing: CGFloat {
+ switch self {
+ case .rounded, .small, .medium:
+ BKSpacing.spacing1
+ case .large:
+ BKSpacing.spacing2
+ }
+ }
+
+ /// ๊ธฐ๋ณธ ๋ชจ์๋ฆฌ ๋ฐ๊ฒฝ
+ var cornerRadius: CGFloat {
+ switch self {
+ case .small:
+ BKRadius.xsmall
+ case .medium, .large:
+ BKRadius.small
+ case .rounded:
+ BKRadius.full
+ }
+ }
+
+ /// ๋ฒํผ์ ๋์ด๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ๋ ๋ฅ๊ทผ corner radius ๋ฐํ
+ ///
+ /// - Parameter width: ๋ฒํผ์ ์ค์ ๋์ด
+ /// - Returns: radius ๊ฐ
+ public func cornerRadius(for width: CGFloat) -> CGFloat {
+ switch self {
+ case .small:
+ return BKRadius.xsmall
+ case .medium, .large:
+ return BKRadius.small
+ case .rounded:
+ return width / 2
+ }
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift
new file mode 100644
index 00000000..41f0be73
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonState.swift
@@ -0,0 +1,33 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+/// `BKButton`์ ์๊ฐ์ ์ํ๋ฅผ ๋ํ๋ด๋ ์ด๊ฑฐํ์
๋๋ค.
+///
+/// ๋ฒํผ์ ํ์ฑํ ์ฌ๋ถ ๋ฐ ๊ฐ์กฐ ์ํ์ ๋ฐ๋ผ ๊ฒฐ์ ๋๋ฉฐ, ์คํ์ผ ๋ ๋๋ง์ ์ฌ์ฉ๋ฉ๋๋ค.
+public enum BKButtonState {
+ /// ๊ธฐ๋ณธ(normal) ์ํ
+ case normal
+
+ /// ํฐ์น ๊ฐ์กฐ(pressed) ์ํ
+ case pressed
+
+ /// ๋นํ์ฑํ(disabled) ์ํ
+ case disabled
+
+ /// ๋ฒํผ์ `isEnabled`์ `isHighlighted` ์์ฑ์ ๋ฐํ์ผ๋ก
+ /// ์ ์ ํ ์ํ๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
+ ///
+ /// - Parameters:
+ /// - isEnabled: ๋ฒํผ์ด ํ์ฑํ๋์ด ์๋์ง ์ฌ๋ถ
+ /// - isHighlighted: ๋ฒํผ์ด ๊ฐ์กฐ ์ํ์ธ์ง ์ฌ๋ถ
+ public init(isEnabled: Bool, isHighlighted: Bool) {
+ if !isEnabled {
+ self = .disabled
+ } else if isHighlighted {
+ self = .pressed
+ } else {
+ self = .normal
+ }
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift b/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift
new file mode 100644
index 00000000..c59dbdea
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Components/Button/BKButtonStyle.swift
@@ -0,0 +1,123 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+/// ๋ฒํผ์ ์๊ฐ์ ์คํ์ผ์ ์ ์ํ๋ ์ด๊ฑฐํ์
๋๋ค.
+///
+/// ๊ฐ ์คํ์ผ์ ๊ณ ์ ํ ๋ฐฐ๊ฒฝ์ ๋ฐ ํ
์คํธ ์์์ ๊ฐ์ง๋ฉฐ,
+/// ๋ฒํผ์ ๋ชฉ์ ๊ณผ ์ฌ์ฉ ๋งฅ๋ฝ์ ๋ฐ๋ผ ์ ์ ํ ์๊ฐ์ ํํ์ ์ ๊ณตํฉ๋๋ค.
+public enum BKButtonStyle: Equatable {
+ /// ๊ฐํ ๊ฐ์กฐ์ ๊ธฐ๋ณธ ๋ฒํผ ์คํ์ผ
+ case primary
+
+ /// ์ค๊ฐ ๊ฐ์กฐ์ ๋ณด์กฐ ๋ฒํผ ์คํ์ผ
+ case secondary
+
+ /// ๊ฐ์ฅ ๋ฎ์ ๊ฐ์กฐ์ ํ
์คํธ ์ค์ฌ ์คํ์ผ
+ case tertiary
+
+ /// ์ง์ ์์์ ์ ์ํ๋ ์ปค์คํ
์คํ์ผ
+ case custom(background: BKButtonColorSet, foreground: BKButtonColorSet)
+
+ /// ์คํ์ผ์ ๋ฐ๋ฅธ ๋ฐฐ๊ฒฝ์ ์ธํธ์
๋๋ค.
+ ///
+ /// ๋ฒํผ ์ํ๋ณ ์์(normal, pressed, disabled)์ ํฌํจํฉ๋๋ค.
+ var backgroundColors: BKButtonColorSet {
+ switch self {
+ case .primary:
+ return BKButtonColorSet(
+ normal: .bkBackgroundColor(.primary),
+ pressed: .bkBackgroundColor(.primaryPressed),
+ disabled: .bkBackgroundColor(.disable)
+ )
+
+ case .secondary:
+ return BKButtonColorSet(
+ normal: .bkBackgroundColor(.secondary),
+ pressed: .bkBackgroundColor(.secondaryPressed),
+ disabled: .bkBackgroundColor(.disable)
+ )
+
+ case .tertiary:
+ return BKButtonColorSet(
+ normal: .bkBackgroundColor(.tertiary),
+ pressed: .bkBackgroundColor(.tertiaryPressed),
+ disabled: .bkBackgroundColor(.disable)
+ )
+
+ case .custom(let background, _):
+ return background
+ }
+ }
+
+ /// ์คํ์ผ์ ๋ฐ๋ฅธ ํ
์คํธ ์์ ์ธํธ์
๋๋ค.
+ ///
+ /// ๋ฒํผ ์ํ๋ณ ์์(normal, pressed, disabled)์ ํฌํจํฉ๋๋ค.
+ var foregroundColors: BKButtonColorSet {
+ switch self {
+ case .primary:
+ return BKButtonColorSet(
+ normal: .bkContentColor(.inverse),
+ pressed: .bkContentColor(.inverse),
+ disabled: .bkContentColor(.disable)
+ )
+
+ case .secondary:
+ return BKButtonColorSet(
+ normal: .bkContentColor(.primary),
+ pressed: .bkContentColor(.primary),
+ disabled: .bkContentColor(.disable)
+ )
+
+ case .tertiary:
+ return BKButtonColorSet(
+ normal: .bkContentColor(.brand),
+ pressed: .bkContentColor(.brand),
+ disabled: .bkContentColor(.disable)
+ )
+
+ case .custom(_, let foreground):
+ return foreground
+ }
+ }
+}
+
+/// ๋ฒํผ์ ์ํ๋ณ ์์ ์ธํธ๋ฅผ ์ ์ํ๋ ๊ตฌ์กฐ์ฒด์
๋๋ค.
+public struct BKButtonColorSet {
+ /// ๊ธฐ๋ณธ(normal) ์ํ์์์ ์์
+ let normal: UIColor
+
+ /// ๋๋ฆผ(pressed) ์ํ์์์ ์์
+ let pressed: UIColor
+
+ /// ๋นํ์ฑํ(disabled) ์ํ์์์ ์์
+ let disabled: UIColor
+
+ /// ๋ฒํผ์ ์ํ์ ๋ง๋ ์์์ ๋ฐํํฉ๋๋ค.
+ ///
+ /// - Parameter state: ๋ฒํผ์ ํ์ฌ ์ํ
+ /// - Returns: ํด๋น ์ํ์ ๋ง๋ ์์
+ public func color(for state: BKButtonState) -> UIColor {
+ switch state {
+ case .normal: return normal
+ case .pressed: return pressed
+ case .disabled: return disabled
+ }
+ }
+
+ /// ๋จ์ผ ์์์ ๋ชจ๋ ์ํ์ ์ ์ฉํ๋ ๊ฐ๋จ ์์ฑ์
+ public static func solid(_ color: UIColor) -> BKButtonColorSet {
+ BKButtonColorSet(normal: color, pressed: color, disabled: color)
+ }
+}
+
+extension BKButtonColorSet: Equatable {
+ public static func == (
+ lhs: BKButtonColorSet,
+ rhs: BKButtonColorSet
+ ) -> Bool {
+ return lhs.normal.isEqual(to: rhs.normal) &&
+ lhs.pressed.isEqual(to: rhs.pressed) &&
+ lhs.disabled.isEqual(to: rhs.disabled)
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift b/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift
new file mode 100644
index 00000000..72e02987
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Extensions/SetNeeds.swift
@@ -0,0 +1,57 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import Foundation
+import UIKit
+
+@propertyWrapper
+public struct SetNeeds {
+ enum Need {
+ case layout
+ case display
+ }
+
+ private var value: Value
+ private let needs: Set
+ private weak var view: UIView?
+
+ // ์ด๊ธฐํ
+ init(
+ wrappedValue: Value,
+ _ needs: Need...
+ ) {
+ self.value = wrappedValue
+ self.needs = Set(needs)
+ self.view = nil
+ }
+
+ // ์ค์ ๊ฐ์ ์ ๊ทผํ ๋ ์ฌ์ฉ๋๋ ํ๋กํผํฐ
+ public var wrappedValue: Value {
+ get { value }
+ set {
+ let oldValue = value
+ value = newValue
+
+ // ๊ฐ์ด ๋ณ๊ฒฝ๋์์ ๋๋ง UI ์
๋ฐ์ดํธ
+ if oldValue != newValue {
+ updateView()
+ }
+ }
+ }
+
+ // ๋ทฐ๋ฅผ ์ค์ ํ๋ ๋ฉ์๋
+ public mutating func configure(with view: UIView) {
+ self.view = view
+ }
+
+ private func updateView() {
+ guard let view = view else { return }
+
+ // needs์ ๋ฐ๋ผ ์ ์ ํ ๋ฉ์๋ ํธ์ถ
+ if needs.contains(.layout) {
+ view.setNeedsLayout() // ๋ ์ด์์ ์ฌ๊ณ์ฐ ํ์
+ }
+ if needs.contains(.display) {
+ view.setNeedsDisplay() // ๋ค์ ๊ทธ๋ฆฌ๊ธฐ ํ์
+ }
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift b/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift
index eb718ee9..f31e15e0 100644
--- a/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift
+++ b/src/Projects/BKDesign/Sources/Extensions/UIColor+.swift
@@ -101,3 +101,10 @@ public extension UIColor {
}
}
}
+
+// UIColor ๋น๊ต๋ฅผ ์ํ ํ์ฅ
+extension UIColor {
+ func isEqual(to color: UIColor) -> Bool {
+ return self.cgColor.__equalTo(color.cgColor)
+ }
+}
diff --git a/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift b/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift
new file mode 100644
index 00000000..70b55730
--- /dev/null
+++ b/src/Projects/BKDesign/Sources/Extensions/UIImage+.swift
@@ -0,0 +1,17 @@
+// Copyright ยฉ 2025 Booket. All rights reserved
+
+import UIKit
+
+extension UIImage {
+ /// Asset ์ด๋ฏธ์ง๋ฅผ ๋ฒํผ ํฌ๊ธฐ์ ๋ง๊ฒ ์กฐ์
+ func resize(to targetSize: CGSize) -> UIImage? {
+ let renderer = UIGraphicsImageRenderer(
+ size: targetSize,
+ format: UIGraphicsImageRendererFormat.preferred()
+ )
+
+ return renderer.image { context in
+ self.draw(in: CGRect(origin: .zero, size: targetSize))
+ }
+ }
+}