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
4 changes: 4 additions & 0 deletions Keychy/Keychy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -475,6 +476,7 @@
3828F5462EC4CCDC00F1B040 /* CollectionView+NormalMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+NormalMode.swift"; sourceTree = "<group>"; };
3828F5482EC4CCE400F1B040 /* CollectionView+SearchMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+SearchMode.swift"; sourceTree = "<group>"; };
3828F54A2EC4D0C500F1B040 /* CollectionView+Handlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionView+Handlers.swift"; sourceTree = "<group>"; };
38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTooltipView.swift; sourceTree = "<group>"; };
385425BD2EB2989400A06C02 /* CollectionCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCellView.swift; sourceTree = "<group>"; };
385425BF2EB2AE7800A06C02 /* CollectionViewModel+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewModel+Sort.swift"; sourceTree = "<group>"; };
385425C22EB2C35E00A06C02 /* WidgetSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetSettingView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -960,6 +962,7 @@
38A22A9C2EC27AC400B4C7C5 /* PackagedKeyringView+SaveImage.swift */,
3822DDC12EBBC712003125BE /* KeyringEditView.swift */,
3822DDBF2EBB2353003125BE /* KeyringMenu.swift */,
38489EBC2F290BD000E41FAE /* CopyTooltipView.swift */,
);
path = Detail;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ extension CollectionKeyringDetailView {
},
onDelete: {
handleMenuDelete()
},
onWidget: {
goToWidgetOnboarding()
}
)
.zIndex(50)
Expand Down Expand Up @@ -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)
}
}

}
Original file line number Diff line number Diff line change
@@ -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()
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

뷰가 넘 깔끔하고 보기 좋다!

237 changes: 166 additions & 71 deletions Keychy/Keychy/Presentation/Collection/Views/Detail/KeyringMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Loading