diff --git a/Keychy/Keychy.xcodeproj/project.pbxproj b/Keychy/Keychy.xcodeproj/project.pbxproj index e64fa0fd..a5e7a776 100644 --- a/Keychy/Keychy.xcodeproj/project.pbxproj +++ b/Keychy/Keychy.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 3828F5472EC4CCDC00F1B040 /* CollectionView+NormalMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F5462EC4CCDC00F1B040 /* CollectionView+NormalMode.swift */; }; 3828F5492EC4CCE400F1B040 /* CollectionView+SearchMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F5482EC4CCE400F1B040 /* CollectionView+SearchMode.swift */; }; 3828F54B2EC4D0C500F1B040 /* CollectionView+Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3828F54A2EC4D0C500F1B040 /* CollectionView+Handlers.swift */; }; + 38489EBD2F290BD000E41FAE /* CopyTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */; }; 385425BE2EB2989400A06C02 /* CollectionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425BD2EB2989400A06C02 /* CollectionCellView.swift */; }; 385425C02EB2AE7800A06C02 /* CollectionViewModel+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425BF2EB2AE7800A06C02 /* CollectionViewModel+Sort.swift */; }; 385425C32EB2C35E00A06C02 /* WidgetSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385425C22EB2C35E00A06C02 /* WidgetSettingView.swift */; }; @@ -475,6 +476,7 @@ 3828F5462EC4CCDC00F1B040 /* CollectionView+NormalMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+NormalMode.swift"; sourceTree = ""; }; 3828F5482EC4CCE400F1B040 /* CollectionView+SearchMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+SearchMode.swift"; sourceTree = ""; }; 3828F54A2EC4D0C500F1B040 /* CollectionView+Handlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+Handlers.swift"; sourceTree = ""; }; + 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTooltipView.swift; sourceTree = ""; }; 385425BD2EB2989400A06C02 /* CollectionCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCellView.swift; sourceTree = ""; }; 385425BF2EB2AE7800A06C02 /* CollectionViewModel+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewModel+Sort.swift"; sourceTree = ""; }; 385425C22EB2C35E00A06C02 /* WidgetSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetSettingView.swift; sourceTree = ""; }; @@ -960,6 +962,7 @@ 38A22A9C2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift */, 3822DDC12EBBC712003125BE /* KeyringEditView.swift */, 3822DDBF2EBB2353003125BE /* KeyringMenu.swift */, + 38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */, ); path = Detail; sourceTree = ""; @@ -2544,6 +2547,7 @@ 4C4733A62F1FA388005D2376 /* KeyringCompleteView+VideoGen.swift in Sources */, 4C4733A72F1FA388005D2376 /* KeyringCompleteView.swift in Sources */, 4C4733A82F1FA388005D2376 /* AcrylicPhotoVM+ImageLoad.swift in Sources */, + 38489EBD2F290BD000E41FAE /* CopyTooltipView.swift in Sources */, 4C4733A92F1FA388005D2376 /* NeonSignPreview.swift in Sources */, 4C4733AA2F1FA388005D2376 /* SpeechBubbleFrameSelectorView.swift in Sources */, 4C4733AB2F1FA388005D2376 /* KeyringViewModelProtocol+Reset.swift in Sources */, diff --git a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift index cc18e3f1..c15cd122 100644 --- a/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift +++ b/Keychy/Keychy/Core/DesignSystem/Typography/Typography.swift @@ -74,6 +74,7 @@ struct Typography { static let nanum18EB = Typography(font: .custom(.nanumExtraBold, size: 18), lineSpacing: 0) static let nanum17EB = Typography(font: .custom(.nanumExtraBold, size: 17), lineSpacing: 0) static let nanum16EB = Typography(font: .custom(.nanumExtraBold, size: 16), lineSpacing: 0) + static let nanum12EB = Typography(font: .custom(.nanumExtraBold, size: 12), lineSpacing: 0) static let nanum15EB25 = Typography(font: .custom(.nanumExtraBold, size: 15), lineSpacing: 10) static let nanum15B25 = Typography(font: .custom(.nanumBold, size: 15), lineSpacing: 10) diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift index b915e440..bb8abfb8 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/CollectionKeyringDetailView+Menu.swift @@ -39,6 +39,9 @@ extension CollectionKeyringDetailView { }, onDelete: { handleMenuDelete() + }, + onWidget: { + goToWidgetOnboarding() } ) .zIndex(50) @@ -83,5 +86,15 @@ extension CollectionKeyringDetailView { showDeleteAlert = true } } + + // MARK: - 위젯 설정 화면으로 이동 + private func goToWidgetOnboarding() { + isSheetPresented = false + showMenu = false + + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + router.push(.widgetSettingView) + } + } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift new file mode 100644 index 00000000..81c74199 --- /dev/null +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/CopyTooltipView.swift @@ -0,0 +1,59 @@ +// +// CopyTooltipView.swift +// Keychy +// +// Created by Jini on 1/28/26. +// + +import SwiftUI + +// 툴팁 말풍선 View +struct CopyTooltipView: View { + + @State private var isAppearing = false + + var body: some View { + VStack(spacing: 0) { + // 삼각형 (위쪽 꼬리) + Triangle() + .fill(Color.white100) + .frame(width: 36, height: 15) + .offset(x: 82, y: 2) + + // 말풍선 내용 + Text("내가 만든 키링만 복사할 수 있어요.") + .typography(.suit15SB25) + .foregroundColor(.black100) + .padding(.horizontal, 15) + .padding(.vertical, 13) + .background(.white100) + .cornerRadius(13) + } + .frame(width: 235, height: 45) + .compositingGroup() + .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 3) + .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .top) + .opacity(isAppearing ? 1.0 : 0.0) + .onAppear { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + isAppearing = true + } + } + } +} + +// 삼각형 Shape +struct Triangle: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: rect.midX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.closeSubpath() + return path + } +} + +#Preview { + CopyTooltipView() +} diff --git a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift index 5c7111fc..54beae51 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift @@ -13,94 +13,189 @@ struct KeyringMenu: View { let onEdit: () -> Void let onCopy: () -> Void let onDelete: () -> Void + let onWidget: () -> Void - private let menuWidth: CGFloat = 165 - private var menuHeight: CGFloat { - isMyKeyring ? 170 : 115 // 복사 버튼 있으면 170, 없으면 115 - } + private let menuWidth: CGFloat = 200 + private let menuHeight: CGFloat = 218 @State private var isAppearing = false + @State private var showCopyTooltip = false + @State private var questionButtonFrame: CGRect = .zero var body: some View { GeometryReader { geometry in ZStack { // 메뉴 - VStack(alignment: .leading, spacing: 5) { - // 편집 버튼 - Button(action: onEdit) { - HStack(spacing: 8) { - Image(.pencil) - .resizable() - .frame(width: 25, height: 25) - - Text("정보 수정") - .typography(.suit16M) - .foregroundColor(.gray600) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .frame(maxWidth: .infinity, alignment: .leading) + menuContent + .position( + x: geometry.size.width - menuWidth / 2 - 16, + y: position.maxY + 8 + menuHeight / 2 + ) + + // 툴팁 말풍선 + if showCopyTooltip { + CopyTooltipView() + .position( + x: geometry.size.width - 125, + y: questionButtonFrame.maxY + 32 + ) + .zIndex(1000) + } + } + } + .onAppear { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + isAppearing = true + } + } + // 툴팁 외부 클릭 시 닫기 + .onTapGesture { + if showCopyTooltip { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showCopyTooltip = false + } + } + } + } + + private var menuContent: some View { + VStack(alignment: .leading, spacing: 5) { + // 편집 버튼 + Button(action: onEdit) { + HStack(spacing: 8) { + Image(.pencil) + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25) + .foregroundColor(.gray600) - if isMyKeyring { - // 복사 버튼 - Button(action: onCopy) { - HStack(spacing: 8) { - Image(.copy) - .resizable() - .frame(width: 25, height: 25) - - Text("복사") - .typography(.suit16M) - .foregroundColor(.gray600) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) - } - .frame(maxWidth: .infinity, alignment: .leading) - } + Text("정보 수정") + .typography(.suit16M) + .foregroundColor(.gray600) - // 삭제 버튼 - Button(action: onDelete) { - HStack(spacing: 8) { - Image(.trash) - .resizable() - .frame(width: 25, height: 25) - - Text("삭제") - .typography(.suit16M) - .foregroundColor(.pink) + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) + + // 복사 버튼 + HStack(spacing: 8) { + Image(.copy) + .renderingMode(.template) + .resizable() + .frame(width: 25, height: 25) + .foregroundColor(isMyKeyring ? .gray600 : .gray300) + + Text("복사") + .typography(.suit16M) + .foregroundColor(isMyKeyring ? .gray600 : .gray300) + + Spacer() + + if !isMyKeyring { + Button { + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + showCopyTooltip.toggle() + } + } label: { + ZStack { + Circle() + .fill(Color.gray500) + .frame(width: 15, height: 15) - Spacer() + Text("?") + .typography(.nanum12EB) + .foregroundColor(.white) } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .contentShape(Rectangle()) } - .frame(maxWidth: .infinity, alignment: .leading) + // 물음표 버튼의 위치 추적 + .background( + GeometryReader { geo in + Color.clear + .preference( + key: QuestionButtonPreferenceKey.self, + value: geo.frame(in: .global) + ) + } + ) + } + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) + .frame(maxWidth: .infinity, alignment: .leading) + .onTapGesture { + if isMyKeyring { + onCopy() + } + } + + // 삭제 버튼 + Button(action: onDelete) { + HStack(spacing: 8) { + Image(.trash) + .resizable() + .frame(width: 25, height: 25) + + Text("삭제") + .typography(.suit16M) + .foregroundColor(.pink) + + Spacer() } + .padding(.vertical, 10) .padding(.horizontal, 10) - .padding(.vertical, 20) - .frame(width: menuWidth, height: menuHeight) - .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) - .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .topTrailing) - .opacity(isAppearing ? 1.0 : 0.0) - .position( - x: geometry.size.width - menuWidth / 2 - 16, - y: position.maxY + 8 + menuHeight / 2 - ) + .contentShape(Rectangle()) } - } - .onAppear { - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - isAppearing = true + .frame(maxWidth: .infinity, alignment: .leading) + + // 구분선 + Rectangle() + .fill(Color.gray100) + .padding(.horizontal, 10) + .frame(height: 1) + + // 위젯 버튼 + Button(action: onWidget) { + HStack(spacing: 8) { + Image(.widget) + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.gray600) + + Text("위젯 설정") + .typography(.suit16M) + .foregroundColor(.gray600) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 10) + .contentShape(Rectangle()) } + .frame(maxWidth: .infinity, alignment: .leading) } + .padding(.horizontal, 10) + .padding(.vertical, 20) + .frame(width: menuWidth, height: menuHeight) + .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 34)) + .scaleEffect(isAppearing ? 1.0 : 0.8, anchor: .topTrailing) + .opacity(isAppearing ? 1.0 : 0.0) + .onPreferenceChange(QuestionButtonPreferenceKey.self) { frame in + questionButtonFrame = frame + } + } +} + +// 물음표 버튼 위치 추적용 PreferenceKey +struct QuestionButtonPreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() } } diff --git a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift index 25b93289..124c7abf 100644 --- a/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift +++ b/Keychy/Keychy/Presentation/Collection/Views/Main/CollectionView+NormalMode.swift @@ -95,15 +95,15 @@ extension CollectionView { .padding(.trailing, 10) #endif - CircleGlassButton(imageName: "Widget", - action: { - isSearchFieldFocused = false - showSearchBar = false - - router.push(.widgetSettingView) - } - ) - .padding(.trailing, 10) +// CircleGlassButton(imageName: "Widget", +// action: { +// isSearchFieldFocused = false +// showSearchBar = false +// +// router.push(.widgetSettingView) +// } +// ) +// .padding(.trailing, 10) CircleGlassButton(imageName: "BundleIcon", action: {