Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
687 changes: 687 additions & 0 deletions ABOUT_MODULE.MD

Large diffs are not rendered by default.

741 changes: 741 additions & 0 deletions CLAUDME.MD

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Common/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ let package = Package(
name: "Util",
targets: ["Util"]
),
.library(
name: "SharedUI",
targets: ["SharedUI"]
),
],
targets: [
.target(
Expand All @@ -42,6 +46,10 @@ let package = Package(
.target(name: "DataSources"),
.target(name: "LocalizedString"),
.target(name: "Util"),
.target(
name: "SharedUI",
dependencies: ["DesignSystem"]
),

.plugin(
name: "ColorGenerator",
Expand Down
54 changes: 54 additions & 0 deletions Common/Sources/DesignSystem/ProfileImageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// ProfileImageView.swift
// Common
//
// Created by 강동영 on 1/11/26.
//

import SwiftUI

public struct ProfileImageView: View {
private let imageUrlString: String
private var width: CGFloat
private var height: CGFloat
private var fillColor: Color

public init(
with imageUrlString: String,
width: CGFloat = 32,
height: CGFloat = 32,
fillColor: Color = Color.bgG200
) {
self.imageUrlString = imageUrlString
self.width = width
self.height = height
self.fillColor = fillColor
}

public var body: some View {
AsyncImage(url: URL(string: imageUrlString)) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height)
case .empty, .failure:
Image(.placeholderProfile)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height)
@unknown default:
Image(.placeholderProfile)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: width, height: height) }
}
}
}

public extension ProfileImageView {
func applyCilpShape() -> some View {
self.clipShape(Circle())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "기본 프로필 이미지.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Common/Sources/Managers/AppStorageKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public extension String {
struct Storage {
static let hasSeenOnboarding = "hasSeenOnboarding"
static let userResponse = "userResponse"
static let currentUserId = "currentUserId"
}
}

34 changes: 27 additions & 7 deletions Common/Sources/Managers/UserDefaultsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,44 @@

import Foundation

// Helper protocol for optional handling
protocol OptionalProtocol {
var isNil: Bool { get }
}

extension Optional: OptionalProtocol {
var isNil: Bool { self == nil }
}

@propertyWrapper
struct UDDefaultWrapper<T> {
public struct UDDefaultWrapper<T> {
private let ud = UserDefaults.standard
var key: String
var defaultValue: T
var wrappedValue: T {
let key: String
let defaultValue: T

public var wrappedValue: T {
get {
return ud.value(forKey: key) as? T ?? defaultValue
}
set {
ud.setValue(newValue, forKey: key)
nonmutating set {
if let optional = newValue as? (any OptionalProtocol), optional.isNil {
ud.removeObject(forKey: key)
} else {
ud.setValue(newValue, forKey: key)
}
ud.synchronize()
}
}
}

public struct UserDefaultsManager {
public final class UserDefaultsManager {
public static let shared = UserDefaultsManager()

private init() {}

@UDDefaultWrapper(key: .Storage.hasSeenOnboarding, defaultValue: false)
var isOnboardingCompleted: Bool

@UDDefaultWrapper(key: .Storage.currentUserId, defaultValue: nil)
public var currentUserId: Int64?
}
98 changes: 98 additions & 0 deletions Common/Sources/SharedUI/CustomTabView/CustomTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// CustomTabView.swift
// Hambug
//
// Created by 강동영 on 12/12/25.
//

import DesignSystem
import SwiftUI

public struct CustomTabView<Content: View>: View {
private let tabConfig: [HambugTab]
@ViewBuilder let content: Content

@Binding private var selectedTab: Int
@State private var isTabBarHidden: Bool = false

public var body: some View {
ZStack {
// Content area
TabView(selection: $selectedTab) {
content
.syncTabBarVisibility(with: $isTabBarHidden)
}

VStack {
Spacer()
if !isTabBarHidden { tabView }
}
.ignoresSafeArea(.container, edges: .bottom)


}
}

/// Custom Tab Bar
private var tabView: some View {
HStack(spacing: 0) {
ForEach(tabConfig) { tab in
TabBarItem(
config: tab,
isSelected: selectedTab == tab.id
) {
selectedTab = tab.id
}
}
}
.frame(height: UIScreen.main.bounds.height * 0.11)
.background(
Color.white
.cornerRadius(30, corners: [.topLeft, .topRight])
)
.shadow(color: .black.opacity(0.1), radius: 10, y: -5)
.transition(.move(edge: .bottom).combined(with: .opacity))
}

public init(
selectedTab: Binding<Int>,
tabConfig: [HambugTab] = HambugTab.allCases,
@ViewBuilder content: () -> Content
) {
self._selectedTab = selectedTab
self.tabConfig = tabConfig
self.content = content()
}
}


// MARK: Style 관련 객체들
fileprivate extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}

struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners

func path(in rect: CGRect) -> SwiftUI.Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return SwiftUI.Path(path.cgPath)
}
}


//
//#Preview {
// @Previewable @State var selectedTab = 0
//
// CustomTabView(selectedTab: $selectedTab) {
// Text("Preview")
// }
//}
77 changes: 77 additions & 0 deletions Common/Sources/SharedUI/CustomTabView/HambugTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// HambugTab.swift
// Common
//
// Created by 강동영 on 1/9/26.
//

import SwiftUI

// MARK: CustomTabView 의 HambugTab
extension CustomTabView {
public enum HambugTab: Int, CaseIterable, Identifiable {
case home = 0
case community = 1
case myPage = 2

public var id: Int { rawValue }

var iconName: String {
switch self {
case .home:
"tab_home"
case .community:
"tab_community"
case .myPage:
"tab_user"
}
}

var title: String {
switch self {
case .home:
"홈"
case .community:
"커뮤니티"
case .myPage:
"마이"
}
}
}
}

// MARK: TabBarItem
extension CustomTabView {
struct TabBarItem: View {
private let config: HambugTab
private let isSelected: Bool
private let action: () -> Void

var body: some View {
Button(action: action) {
VStack(spacing: 8) {
Image(config.iconName)
.renderingMode(.template)
.resizable()
.scaledToFit()
.frame(width: 21, height: 21)

Text(config.title)
.pretendard(.caption(.emphasis))
}
.foregroundColor(isSelected ? .primaryHambugRed : .borderG400)
.frame(maxWidth: .infinity)
}
}

init(
config: HambugTab,
isSelected: Bool,
action: @escaping () -> Void
) {
self.config = config
self.isSelected = isSelected
self.action = action
}
}
}
56 changes: 56 additions & 0 deletions Common/Sources/SharedUI/CustomTabView/TabBarVisibilityKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// TabBarVisibilityKey.swift
// Common
//
// Created by 강동영 on 1/9/26.
//

import SwiftUI

// MARK: - TabBar Visibility Environment
private struct TabBarVisibilityKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}

extension EnvironmentValues {
var tabBarVisibility: Binding<Bool> {
get { self[TabBarVisibilityKey.self] }
set { self[TabBarVisibilityKey.self] = newValue }
}
}

public extension View {
/// 커스텀 탭바를 숨기거나 표시합니다.
/// - Parameter hidden: true면 탭바를 숨기고, false면 탭바를 표시합니다.
func tabBarHidden(_ hidden: Bool) -> some View {
self
.onAppear {
// View가 나타날 때 preference를 강제로 다시 전파
// 이렇게 하면 NavigationStack에서 pop 시에도 제대로 갱신됨
}
.preference(key: TabBarVisibilityPreference.self, value: hidden)
}
}

private struct TabBarVisibilityPreference: PreferenceKey {
static var defaultValue: Bool = false

static func reduce(value: inout Bool, nextValue: () -> Bool) {
// 자식 View의 preference가 우선
// true(숨김)가 있으면 true를 우선시
let next = nextValue()
if next {
value = next
}
}
}

extension View {
func syncTabBarVisibility(with binding: Binding<Bool>) -> some View {
self.onPreferenceChange(TabBarVisibilityPreference.self) { newValue in
withAnimation(.easeInOut(duration: 0.2)) {
binding.wrappedValue = newValue
}
}
}
}
Loading
Loading