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 @@ -245,6 +245,7 @@
4C84A1602EB134BD008FFE57 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A596832EAFEAA20003D712 /* ProfileSetupView.swift */; };
4C86A6122F25C0B10023AA2D /* WorkshopBundleGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */; };
4C86A6142F25C0BA0023AA2D /* WorkshopKeyringGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */; };
4C86A6182F276CFD0023AA2D /* WorkshopTemplateSelectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C86A6172F276CFD0023AA2D /* WorkshopTemplateSelectSheet.swift */; };
4CA9C6A62EC9D11600CA546B /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9C6A42EC9D11600CA546B /* CustomNavigationBar.swift */; };
4CA9C6A82EC9DB5300CA546B /* View+SafeAreaBottom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9C6A72EC9DB5300CA546B /* View+SafeAreaBottom.swift */; };
4CA9C6D62ECB7AEA00CA546B /* BadWords.json in Resources */ = {isa = PBXBuildFile; fileRef = 4CA9C6D52ECB7AEA00CA546B /* BadWords.json */; };
Expand Down Expand Up @@ -685,6 +686,7 @@
4C8426632ED375840050B6FE /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = "<group>"; };
4C86A6112F25C0B10023AA2D /* WorkshopBundleGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopBundleGridView.swift; sourceTree = "<group>"; };
4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopKeyringGridView.swift; sourceTree = "<group>"; };
4C86A6172F276CFD0023AA2D /* WorkshopTemplateSelectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopTemplateSelectSheet.swift; sourceTree = "<group>"; };
4CA9C6A42EC9D11600CA546B /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; };
4CA9C6A72EC9DB5300CA546B /* View+SafeAreaBottom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+SafeAreaBottom.swift"; sourceTree = "<group>"; };
4CA9C6D52ECB7AEA00CA546B /* BadWords.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = BadWords.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1439,6 +1441,7 @@
children = (
4C4733E42F20FE34005D2376 /* WorkshopRecentTemplate.swift */,
4C86A6132F25C0BA0023AA2D /* WorkshopKeyringGridView.swift */,
4C86A6172F276CFD0023AA2D /* WorkshopTemplateSelectSheet.swift */,
);
path = Keyring;
sourceTree = "<group>";
Expand Down Expand Up @@ -2589,6 +2592,7 @@
4C4733D52F1FA388005D2376 /* AcrylicPhotoVM+CropBoxDrag.swift in Sources */,
4C4733D62F1FA388005D2376 /* SpeechBubbleVM+Customizing.swift in Sources */,
4C4733D72F1FA388005D2376 /* AcrylicPhotoVM+Crop.swift in Sources */,
4C86A6182F276CFD0023AA2D /* WorkshopTemplateSelectSheet.swift in Sources */,
4C4733D82F1FA388005D2376 /* AcrylicPhotoPreView.swift in Sources */,
4C4733D92F1FA388005D2376 /* TemplatePreviewComponents.swift in Sources */,
4C004FA42F177C4600D9063E /* BundleVideoGenerator+Setup.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,11 @@ struct InvenExpandPopup: View {
Button(action: onConfirm) {
HStack(spacing: 4) {
Image(.myCoinMini)
.resizable()
.frame(width: 34, height: 34)
.padding(.bottom, 4)

Text("\(price)")
.typography(.nanum18EB)
.foregroundColor(.white100)
.frame(height: 32)
.padding(.top, 4)

}
.frame(maxWidth: .infinity)
.frame(height: 48)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct Typography {
// MARK: - Nanum
static let nanum20EB = Typography(font: .custom(.nanumExtraBold, size: 20), lineSpacing: 0)
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 nanum15EB25 = Typography(font: .custom(.nanumExtraBold, size: 15), lineSpacing: 10)
Expand Down
11 changes: 10 additions & 1 deletion Keychy/Keychy/Core/Navigation/Routes/WorkshopRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@
import Foundation

/// 공방 탭 라우팅 (마켓플레이스 + 키링 제작)
enum WorkshopRoute: Hashable {
enum WorkshopRoute: Hashable, BundleRoute {
// MARK: - 공방 마켓플레이스
case workshopPreview(item: AnyHashable)
case coinCharge
case myItems
case workshopTemplates

// MARK: - 번들 만들기
case bundleInventoryView
case bundleDetailView
case bundleCreateView
case bundleAddKeyringView
case bundleNameInputView
case bundleNameEditView
case bundleEditView

// MARK: - Festival 임시 라우트 (추후 정리 예정)
case showcase25BoardView
case festivalKeyringDetailView(Keyring)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class BundleViewModel {

var carabiners: [Carabiner] { dataManager.carabiners }
var selectedCarabiner: Carabiner?

// 공방에서 "뭉치에 사용하기"로 진입 시 미리 선택할 아이템 ID
var preSelectedBackgroundId: String?
var preSelectedCarabinerId: String?

// 뭉치 이름 최대 글자 수
var maxBundleNameCount: Int = 9
Expand Down
40 changes: 29 additions & 11 deletions Keychy/Keychy/Presentation/Bundle/Views/BundleCreateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,27 +281,45 @@ extension BundleCreateView {
// 배경 데이터 로드
await withCheckedContinuation { continuation in
bundleVM.fetchAllBackgrounds { _ in
// "키치 배경"을 기본으로 선택, 없으면 첫 번째 선택
if self.selectedBackground == nil {
self.selectedBackground = bundleVM.backgroundViewData.first { bg in
bg.background.backgroundName == "키치 배경"
} ?? bundleVM.backgroundViewData.first
// 공방에서 미리 선택된 배경이 있으면 해당 배경 선택
if let preSelectedId = bundleVM.preSelectedBackgroundId {
self.selectedBackground = bundleVM.backgroundViewData.first { bg in
bg.background.id == preSelectedId
}
bundleVM.preSelectedBackgroundId = nil // 사용 후 초기화
}
// 미리 선택된 배경이 없으면 "퍼플키치"를 기본으로 선택, 없으면 첫 번째 선택
if self.selectedBackground == nil {
self.selectedBackground = bundleVM.backgroundViewData.first { bg in
bg.background.backgroundName == "퍼플키치"
} ?? bundleVM.backgroundViewData.first
}
}

continuation.resume()
}
}

// 카라비너 데이터 로드
await withCheckedContinuation { continuation in
bundleVM.fetchAllCarabiners { _ in
// "키치 카라비너"를 기본으로 선택, 없으면 첫 번째 선택
if self.selectedCarabiner == nil {
self.selectedCarabiner = bundleVM.carabinerViewData.first { cb in
cb.carabiner.carabinerName == "키치 카라비너"
} ?? bundleVM.carabinerViewData.first
// 공방에서 미리 선택된 카라비너가 있으면 해당 카라비너 선택
if let preSelectedId = bundleVM.preSelectedCarabinerId {
self.selectedCarabiner = bundleVM.carabinerViewData.first { cb in
cb.carabiner.id == preSelectedId
}
bundleVM.preSelectedCarabinerId = nil // 사용 후 초기화
}
// 미리 선택된 카라비너가 없으면 "웰컴 키치"를 기본으로 선택, 없으면 첫 번째 선택
if self.selectedCarabiner == nil {
self.selectedCarabiner = bundleVM.carabinerViewData.first { cb in
cb.carabiner.carabinerName == "웰컴 키치"
} ?? bundleVM.carabinerViewData.first
}
}

continuation.resume()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import SwiftUI
// MARK: - Template Action Button

/// 키링 템플릿 전용 액션 버튼
/// - 보유중이면 "만들기" 버튼 표시 (활성화)
/// - 보유중이면 "키링 만들기" 버튼 표시 (활성화)
/// - 유료이고 미보유면 구매 버튼 표시
/// - 무료이고 미보유면 "만들기" 버튼 표시 (활성화)
/// - 무료이고 미보유면 "키링 만들기" 버튼 표시 (활성화)
struct TemplateActionButton: View {
let template: KeyringTemplate
let isOwned: Bool
Expand All @@ -36,7 +36,7 @@ struct TemplateActionButton: View {
Button {
onMake()
} label: {
Text("만들기")
Text("키링 만들기")
.typography(.suit17B)
.foregroundStyle(.white100)
.frame(maxWidth: .infinity)
Expand All @@ -53,9 +53,6 @@ struct TemplateActionButton: View {
} label: {
HStack(spacing: 5) {
Image(.myCoinMini)
.resizable()
.scaledToFit()
.frame(width: 32)

Text("\(template.workshopPrice)")
.typography(.nanum18EB)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ struct TemplatePreviewBody: View {
} center: {
Spacer()
} trailing: {
Spacer()
coinButton
}
}
.ignoresSafeArea()
Expand Down Expand Up @@ -309,4 +309,23 @@ extension TemplatePreviewBody {
}
}
}

/// 코인 충전 버튼
private var coinButton: some View {
Button {
router?.push(.coinCharge)
} label: {
HStack(spacing: 8) {
Image(.myCoinMini)

Text("\((userManager.currentUser?.coin ?? 0).formatted())")
.typography(.nanum17EB)
.foregroundColor(.black)
}
.padding(.horizontal, 16)
.padding(.vertical, 12.5)
}
.buttonStyle(.plain)
.glassEffect(.regular.interactive(), in: .capsule)
}
}
24 changes: 22 additions & 2 deletions Keychy/Keychy/Presentation/Tab/Views/WorkshopTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ struct WorkshopTab: View {
@State private var speechBubbleVM: SpeechBubbleVM?
@State private var workshopViewModel = WorkshopViewModel(userManager: UserManager.shared)

// Bundle 관련 ViewModel
@State private var collectionViewModel = CollectionViewModel()
@State private var bundleViewModel = BundleViewModel()

var body: some View {
NavigationStack(path: $router.path) {
WorkshopView(
Expand All @@ -42,9 +46,9 @@ struct WorkshopTab: View {
if let template = item.base as? KeyringTemplate {
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: template)
} else if let background = item.base as? Background {
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: background)
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: background, bundleViewModel: bundleViewModel)
} else if let carabiner = item.base as? Carabiner {
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: carabiner)
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: carabiner, bundleViewModel: bundleViewModel)
} else if let particle = item.base as? Particle {
WorkshopItemDetailView(router: router, viewModel: workshopViewModel, item: particle)
} else if let sound = item.base as? Sound {
Expand Down Expand Up @@ -233,6 +237,22 @@ struct WorkshopTab: View {
festivalVM.onKeyringCompleteFromFestival?(router)
} : nil
)

// MARK: - Bundle
case .bundleInventoryView:
BundleInventoryView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleDetailView:
BundleDetailView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleCreateView:
BundleCreateView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleAddKeyringView:
BundleAddKeyringView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleNameInputView:
BundleNameInputView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleNameEditView:
BundleNameEditView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
case .bundleEditView:
BundleEditView(router: router, collectionVM: collectionViewModel, bundleVM: bundleViewModel)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ enum QuickFilter: CaseIterable {
var icon: ImageResource? {
switch self {
case .free: return nil
case .owned: return .quickFilterOwned
case .owned: return .workshopOwnedIcon
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,81 +1,33 @@
//
// WorkshopMakingKeyringSection.swift
// WorkshopBundleBanner.swift
// Keychy
//
// Created by rundo on 11/24/25.
// Created by 길지훈 on 1/25/26.
//

import SwiftUI
import NukeUI
import Combine

// MARK: - MakingKeyring Section
/// 번들 탭 배너 - 1초마다 이미지 전환
struct WorkshopBundleBanner: View {
@State private var currentIndex = 0

extension WorkshopView {
var WorkshopBundleBanner: some View {
VStack(spacing: 0) {
// 제목
Text("내 마음대로 고르는\n다양한 템플릿(๑' ᵕ '๑)⸝*")
.typography(.suit16B)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
.padding(.horizontal, 20)
.padding(.top, 13)
.padding(.bottom, 10)

workshopBannerImage

// 키링 만들기 버튼
Button {
router.push(.workshopTemplates)
} label: {
ZStack {
// 바탕 레이어
RoundedRectangle(cornerRadius: 15)
.fill(Color.main400)
.frame(maxWidth: .infinity)
.frame(height: 48)

// 버튼 제목
Text("+ 키링 만들기")
.typography(.suit17B)
.foregroundStyle(Color.white100)
}
}
.padding(.horizontal, 10)
.padding(.bottom, 6)
}
.background(Color.white50)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white70, lineWidth: 1)
)
.padding(.horizontal, 15)
.padding(.bottom, 12)
}

// MARK: - Workshop Banner Image
private var workshopBannerImage: some View {
ZStack {
// GIF (항상 렌더링 - 백그라운드에서 로드)
if let url = viewModel.workshopBannerURL {
NukeAnimatedImageView(
url: url,
isLoading: $viewModel.isWorkshopBannerLoading,
maxSize: CGSize(width: 1800, height: 1800)
)
}
private let images: [ImageResource] = [
.bundleBanner1,
.bundleBanner2,
.bundleBanner3,
.bundleBanner4
]

// 썸네일 (로딩 중에만 GIF 위에 덮음 - 정지 상태)
if viewModel.isWorkshopBannerLoading, let thumbnailImage = viewModel.workshopThumbnailImage {
Image(uiImage: thumbnailImage)
.resizable()
.scaledToFit()
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

var body: some View {
Image(images[currentIndex])
.resizable()
.aspectRatio(contentMode: .fit)
.onReceive(timer) { _ in
currentIndex = (currentIndex + 1) % images.count
}
}
.frame(height: 120)
.padding(.bottom, 10)
.padding(.horizontal, 8.5)
}
}


Loading