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
40 changes: 30 additions & 10 deletions Keychy/Keychy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
4C4733242F1FA2AB005D2376 /* WorkshopPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733212F1FA2AB005D2376 /* WorkshopPreview.swift */; };
4C4733252F1FA2AB005D2376 /* MyItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733202F1FA2AB005D2376 /* MyItemsView.swift */; };
4C4733262F1FA2AB005D2376 /* WorkshopGridHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733182F1FA2AB005D2376 /* WorkshopGridHelpers.swift */; };
4C4733272F1FA2AB005D2376 /* WorkshopMakingKeyringSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C47331A2F1FA2AB005D2376 /* WorkshopMakingKeyringSection.swift */; };
4C4733272F1FA2AB005D2376 /* WorkshopBundleBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C47331A2F1FA2AB005D2376 /* WorkshopBundleBanner.swift */; };
4C4733282F1FA2AB005D2376 /* WorkshopTemplatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C47331C2F1FA2AB005D2376 /* WorkshopTemplatesView.swift */; };
4C4733292F1FA2AB005D2376 /* WorkshopMainContentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733192F1FA2AB005D2376 /* WorkshopMainContentSection.swift */; };
4C47332A2F1FA2AB005D2376 /* WorkshopStickyHeaderSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C47331B2F1FA2AB005D2376 /* WorkshopStickyHeaderSection.swift */; };
Expand Down Expand Up @@ -197,6 +197,7 @@
4C4733D72F1FA388005D2376 /* AcrylicPhotoVM+Crop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733522F1FA388005D2376 /* AcrylicPhotoVM+Crop.swift */; };
4C4733D82F1FA388005D2376 /* AcrylicPhotoPreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C47335A2F1FA388005D2376 /* AcrylicPhotoPreView.swift */; };
4C4733D92F1FA388005D2376 /* TemplatePreviewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733392F1FA388005D2376 /* TemplatePreviewComponents.swift */; };
4C4733E52F20FE34005D2376 /* WorkshopRecentTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4733E42F20FE34005D2376 /* WorkshopRecentTemplate.swift */; };
4C65303B2EBA5FA0000F8154 /* CheckmarkAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C65303A2EBA5FA0000F8154 /* CheckmarkAlert.swift */; };
4C65303E2EBA6042000F8154 /* ImageSaveAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C65303D2EBA6042000F8154 /* ImageSaveAlert.swift */; };
4C6530442EBA8077000F8154 /* PurchaseSuccessAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6530432EBA8077000F8154 /* PurchaseSuccessAlert.swift */; };
Expand Down Expand Up @@ -549,7 +550,7 @@
4C4733162F1FA2AB005D2376 /* WorkshopViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopViewModel.swift; sourceTree = "<group>"; };
4C4733182F1FA2AB005D2376 /* WorkshopGridHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopGridHelpers.swift; sourceTree = "<group>"; };
4C4733192F1FA2AB005D2376 /* WorkshopMainContentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopMainContentSection.swift; sourceTree = "<group>"; };
4C47331A2F1FA2AB005D2376 /* WorkshopMakingKeyringSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopMakingKeyringSection.swift; sourceTree = "<group>"; };
4C47331A2F1FA2AB005D2376 /* WorkshopBundleBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopBundleBanner.swift; sourceTree = "<group>"; };
4C47331B2F1FA2AB005D2376 /* WorkshopStickyHeaderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopStickyHeaderSection.swift; sourceTree = "<group>"; };
4C47331C2F1FA2AB005D2376 /* WorkshopTemplatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopTemplatesView.swift; sourceTree = "<group>"; };
4C47331D2F1FA2AB005D2376 /* WorkshopTopBannerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopTopBannerSection.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -626,6 +627,7 @@
4C47338B2F1FA388005D2376 /* SpeechBubbleFramePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechBubbleFramePreviewView.swift; sourceTree = "<group>"; };
4C47338C2F1FA388005D2376 /* SpeechBubbleFrameSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechBubbleFrameSelectorView.swift; sourceTree = "<group>"; };
4C47338D2F1FA388005D2376 /* SpeechBubblePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechBubblePreview.swift; sourceTree = "<group>"; };
4C4733E42F20FE34005D2376 /* WorkshopRecentTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopRecentTemplate.swift; sourceTree = "<group>"; };
4C65303A2EBA5FA0000F8154 /* CheckmarkAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkAlert.swift; sourceTree = "<group>"; };
4C65303D2EBA6042000F8154 /* ImageSaveAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaveAlert.swift; sourceTree = "<group>"; };
4C6530432EBA8077000F8154 /* PurchaseSuccessAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseSuccessAlert.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1093,23 +1095,25 @@
4C47331F2F1FA2AB005D2376 /* Main */ = {
isa = PBXGroup;
children = (
4C4733182F1FA2AB005D2376 /* WorkshopGridHelpers.swift */,
4C47331E2F1FA2AB005D2376 /* WorkshopView.swift */,
4C47331D2F1FA2AB005D2376 /* WorkshopTopBannerSection.swift */,
4C4733192F1FA2AB005D2376 /* WorkshopMainContentSection.swift */,
4C47331A2F1FA2AB005D2376 /* WorkshopMakingKeyringSection.swift */,
4C47331B2F1FA2AB005D2376 /* WorkshopStickyHeaderSection.swift */,
4C47331C2F1FA2AB005D2376 /* WorkshopTemplatesView.swift */,
4C47331D2F1FA2AB005D2376 /* WorkshopTopBannerSection.swift */,
4C47331E2F1FA2AB005D2376 /* WorkshopView.swift */,
4C4733182F1FA2AB005D2376 /* WorkshopGridHelpers.swift */,
4C4733202F1FA2AB005D2376 /* MyItemsView.swift */,
4C4733212F1FA2AB005D2376 /* WorkshopPreview.swift */,
);
path = Main;
sourceTree = "<group>";
};
4C4733222F1FA2AB005D2376 /* Views */ = {
isa = PBXGroup;
children = (
4C4733152F1FA2AB005D2376 /* Components */,
4C4733E02F20FD18005D2376 /* Keyring */,
4C4733E22F20FD50005D2376 /* Bundle */,
4C47331F2F1FA2AB005D2376 /* Main */,
4C4733202F1FA2AB005D2376 /* MyItemsView.swift */,
4C4733212F1FA2AB005D2376 /* WorkshopPreview.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -1399,6 +1403,22 @@
path = KeyringMaker;
sourceTree = "<group>";
};
4C4733E02F20FD18005D2376 /* Keyring */ = {
isa = PBXGroup;
children = (
4C4733E42F20FE34005D2376 /* WorkshopRecentTemplate.swift */,
);
path = Keyring;
sourceTree = "<group>";
};
4C4733E22F20FD50005D2376 /* Bundle */ = {
isa = PBXGroup;
children = (
4C47331A2F1FA2AB005D2376 /* WorkshopBundleBanner.swift */,
);
path = Bundle;
sourceTree = "<group>";
};
4C65303C2EBA5FF3000F8154 /* Alerts */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1963,7 +1983,6 @@
4CEC62682EAE08DF0099ECEE /* Workshop */ = {
isa = PBXGroup;
children = (
4C4733152F1FA2AB005D2376 /* Components */,
4C4733172F1FA2AB005D2376 /* ViewModels */,
4C4733222F1FA2AB005D2376 /* Views */,
C665DDE92EAEFAA800CE4495 /* Coin */,
Expand Down Expand Up @@ -2394,6 +2413,7 @@
4CEC621E2EAE08DA0099ECEE /* KeyringChainComponent.swift in Sources */,
4CEC621F2EAE08DA0099ECEE /* KeyringBodyComponent.swift in Sources */,
4CEC62202EAE08DA0099ECEE /* View+Extension.swift in Sources */,
4C4733E52F20FE34005D2376 /* WorkshopRecentTemplate.swift in Sources */,
38173D0C2EB8AD8800E36F7E /* CategoryContextMenu.swift in Sources */,
C6830F172EBB08380059379A /* MultiKeyringScene.swift in Sources */,
C6830F182EBB08380059379A /* MultiKeyringSceneView.swift in Sources */,
Expand Down Expand Up @@ -2560,7 +2580,7 @@
4C4733242F1FA2AB005D2376 /* WorkshopPreview.swift in Sources */,
4C4733252F1FA2AB005D2376 /* MyItemsView.swift in Sources */,
4C4733262F1FA2AB005D2376 /* WorkshopGridHelpers.swift in Sources */,
4C4733272F1FA2AB005D2376 /* WorkshopMakingKeyringSection.swift in Sources */,
4C4733272F1FA2AB005D2376 /* WorkshopBundleBanner.swift in Sources */,
4C4733282F1FA2AB005D2376 /* WorkshopTemplatesView.swift in Sources */,
4C4733292F1FA2AB005D2376 /* WorkshopMainContentSection.swift in Sources */,
4C47332A2F1FA2AB005D2376 /* WorkshopStickyHeaderSection.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Keychy/Keychy/CommonModels/User/KeychyUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct KeychyUser: Identifiable {
var carabiners: [String]
var tags: [String]
var keyrings: [String]
var recentTemplates: [String] // 최근 사용 템플릿 ID (최대 5개, 최신순)
var termsAgreed: Bool // 필수 약관 동의 여부
var marketingAgreed: Bool // 마케팅 수신 동의 여부

Expand All @@ -48,6 +49,7 @@ struct KeychyUser: Identifiable {
"carabiners": carabiners,
"tags": tags,
"keyrings": keyrings,
"recentTemplates": recentTemplates,
"termsAgreed": termsAgreed,
"marketingAgreed": marketingAgreed
]
Expand Down Expand Up @@ -77,6 +79,7 @@ struct KeychyUser: Identifiable {
self.carabiners = data["carabiners"] as? [String] ?? []
self.tags = data["tags"] as? [String] ?? []
self.keyrings = data["keyrings"] as? [String] ?? []
self.recentTemplates = data["recentTemplates"] as? [String] ?? []
self.termsAgreed = data["termsAgreed"] as? Bool ?? false
self.marketingAgreed = data["marketingAgreed"] as? Bool ?? false
}
Expand All @@ -99,6 +102,7 @@ struct KeychyUser: Identifiable {
self.carabiners = []
self.tags = []
self.keyrings = []
self.recentTemplates = []
self.termsAgreed = false
self.marketingAgreed = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ struct Typography {

static let nanum10EB12 = Typography(font: .custom(.nanumExtraBold, size: 10), lineSpacing: 2)
static let nanum18EB12 = Typography(font: .custom(.nanumExtraBold, size: 18), lineSpacing: 2)
static let nanum24EB = Typography(font: .custom(.nanumExtraBold, size: 24), lineSpacing: 0)
static let nanum32EB = Typography(font: .custom(.nanumExtraBold, size: 32), lineSpacing: 2)

// MARK: - Pretendard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ extension KeyringInfoInputView {
soundId: soundId,
particleId: particleId
)

// 최근 사용 템플릿 업데이트
if !selectedTemplate.isEmpty {
self.updateRecentTemplates(uid: uid, templateId: selectedTemplate)
}

completion(true, keyringId)
} else {
completion(false, nil)
Expand Down Expand Up @@ -273,6 +279,44 @@ extension KeyringInfoInputView {
}
}
}

// MARK: - 최근 사용 템플릿 업데이트
/// 새 템플릿을 맨 앞에 추가하고, 중복 제거 후 최대 10개 유지
private func updateRecentTemplates(uid: String, templateId: String) {
let userRef = db.collection("User").document(uid)

userRef.getDocument { snapshot, error in
guard let data = snapshot?.data(),
error == nil else {
print("[RecentTemplates] 문서 읽기 실패: \(error?.localizedDescription ?? "")")
return
}

var recentTemplates = data["recentTemplates"] as? [String] ?? []

// 1. 이미 있으면 제거 (중복 방지)
recentTemplates.removeAll { $0 == templateId }

// 2. 맨 앞에 추가
recentTemplates.insert(templateId, at: 0)

// 3. 최대 5개 유지
if recentTemplates.count > 5 {
recentTemplates = Array(recentTemplates.prefix(5))
}

// 4. Firebase 업데이트
userRef.updateData([
"recentTemplates": recentTemplates
]) { error in
if let error = error {
print("[RecentTemplates] 업데이트 실패: \(error.localizedDescription)")
} else {
print("[RecentTemplates] 업데이트 성공: \(templateId)")
}
}
}
}

// MARK: - 위젯용 이미지 캡처 및 캐싱
private func captureAndCacheKeyring(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ class WorkshopViewModel {
}
}

/// 최근 사용 템플릿 (최신순, 최대 5개)
var recentTemplates: [KeyringTemplate] {
guard let user = userManager.currentUser else { return [] }
// recentTemplates ID 순서대로 템플릿 객체 반환
return user.recentTemplates.compactMap { templateId in
templates.first { $0.id == templateId }
}
}

private var userManager: UserManager

init(userManager: UserManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import NukeUI
// MARK: - MakingKeyring Section

extension WorkshopView {
var makingKeyringSection: some View {
var WorkshopBundleBanner: some View {
VStack(spacing: 0) {
// 제목
Text("내 마음대로 고르는\n다양한 템플릿(๑' ᵕ '๑)⸝*")
Expand Down Expand Up @@ -72,8 +72,6 @@ extension WorkshopView {
.resizable()
.scaledToFit()
}

// TODO: 네트워크 연결 끊김 처리
}
.frame(height: 120)
.padding(.bottom, 10)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// WorkshopRecentTemplate.swift
// Keychy
//
// Created by 길지훈 on 1/21/26.
//

import SwiftUI
import NukeUI

// MARK: - 최근 사용 템플릿 섹션

struct WorkshopRecentTemplate: View {
let templates: [KeyringTemplate]
let isLoading: Bool
var onTemplateTap: ((KeyringTemplate) -> Void)?

var body: some View {
VStack(alignment: .leading, spacing: 12) {
// 섹션 타이틀
Text("최근 사용 템플릿")
.typography(.suit17B)
.foregroundColor(.black100)
.padding(.horizontal, 20)

// 콘텐츠
if isLoading {
loadingView
} else if templates.isEmpty {
recentEmptyView
} else {
templateScrollView
}
}
}

// MARK: - 로딩 뷰

private var loadingView: some View {
HStack(spacing: 12) {
ForEach(0..<3, id: \.self) { _ in
SkeletonBox(width: 112, height: 112)
}
}
.padding(.horizontal, 20)
}

// MARK: - 빈 상태 뷰
private var recentEmptyView: some View {
HStack {
Spacer()
Text("키링을 만들면 최근 사용한 템플릿이 이곳에 표시됩니다")
.typography(.suit14R18)
.foregroundColor(.gray500)
.multilineTextAlignment(.center)
Spacer()
}
.frame(height: 100)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(.white70)
)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray50, lineWidth: 1)
)
.padding(.horizontal, 20)
}

// MARK: - 템플릿 스크롤 뷰

private var templateScrollView: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 7) {
ForEach(templates, id: \.id) { template in
RecentTemplateCard(template: template) {
onTemplateTap?(template)
}
}
}
.padding(.horizontal, 20)
}
}
}

// MARK: - 개별 템플릿 카드
private struct RecentTemplateCard: View {
let template: KeyringTemplate
let onTap: () -> Void

var body: some View {
Button(action: onTap) {
ZStack {
LazyImage(url: URL(string: template.thumbnailURL)) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else if state.isLoading {
LoadingAlert(type: .short30, message: nil)
} else {
Color.gray50
.frame(width: 112, height: 112)
}
}
.padding(5)

// 유료 아이콘
if !template.isFree {
VStack {
HStack {
Image(.myCoinMini)

Spacer()
}
.padding(.top, 7)
.padding(.leading, 7)
Spacer()
}
}
}
.frame(width: 112, height: 112)
.background(Color.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(.gray50, lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}

// MARK: - Preview

#Preview("로딩 상태") {
WorkshopRecentTemplate(
templates: [],
isLoading: true
)
}

#Preview("빈 상태") {
WorkshopRecentTemplate(
templates: [],
isLoading: false
)
}
Loading