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
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ChatGPT Image 2025년 12월 12일 오후 02_55_30 1.png",
"filename" : "travelList.png",
"idiom" : "universal",
"scale" : "1x"
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "lucide_files.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "lucide_files@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "lucide_files@3x.png",
"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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "lucide_upload.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "lucide_upload@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "lucide_upload@3x.png",
"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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions DesignSystem/Sources/Image/ImageAsset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ public enum ImageAsset: String {
case travelDetailMock
case supportMaIl
case settlementEmpty
case copy
case upload
}
34 changes: 34 additions & 0 deletions DesignSystem/Sources/Utilities/InviteCodeHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// InviteCodeHelper.swift
// DesignSystem
//
// Created by Claude on 12/17/24.
//

import UIKit

/// 여행 초대 코드 복사 및 공유 기능을 제공하는 헬퍼
@MainActor
public enum InviteCodeHelper {

/// 초대 코드를 클립보드에 복사
/// - Parameter code: 복사할 초대 코드
public static func copyToClipboard(_ code: String) {
UIPasteboard.general.string = code
ToastManager.shared.showSuccess("클립보드에 복사되었습니다.")
}

/// 딥링크를 시스템 공유 시트를 통해 공유
/// - Parameter deepLink: 공유할 딥링크 URL
public static func shareDeepLink(_ deepLink: URL) {
let activityViewController = UIActivityViewController(
activityItems: [deepLink],
applicationActivities: nil
)

if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
window.rootViewController?.present(activityViewController, animated: true)
}
}
}
34 changes: 34 additions & 0 deletions DesignSystem/Sources/View/EmptyCaseView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// EmptyCaseView.swift
// DesignSystem
//
// Created by 홍석현 on 12/16/25.
//

import SwiftUI

public struct EmptyCaseView: View {
private let image: ImageAsset
private let message: String

public init(image: ImageAsset, message: String) {
self.image = image
self.message = message
}

public var body: some View {
VStack(spacing: 16) {
Image(asset: image)
.resizable()
.frame(width: 167, height: 167)
Text(message)
.font(.app(.title3, weight: .medium))
.foregroundStyle(Color.black)
Copy link
Contributor

Choose a reason for hiding this comment

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

@Peter1119 .black 만 해도 될거 같아요!

}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

#Preview {
EmptyCaseView(image: .expenseEmpty, message: "아직 지출이 없어요")
}
2 changes: 1 addition & 1 deletion Domain/Sources/Router/DeeplinkRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

import Foundation
import ComposableArchitecture
import Dependencies
import LogMacro

public struct DeeplinkRouter: Sendable {
Expand Down
215 changes: 215 additions & 0 deletions Features/ExpenseList/Sources/Components/DateRangePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//
// DateRangePicker.swift
// ExpenseListFeature
//
// Created by Claude on 12/16/24.
//

import SwiftUI
import UIKit

// MARK: - DateRangePicker (SwiftUI Wrapper)

struct DateRangePicker: View {
let startDate: Date
let endDate: Date
@Binding var selectedRange: ClosedRange<Date>?
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationView {
Copy link
Contributor

Choose a reason for hiding this comment

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

@Peter1119 여기에서 NavigationView 를 쓰는 이유는 무엇인가요 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

navigation title 때문이었습니다 ~!

CalendarView(
startDate: startDate,
endDate: endDate,
selectedRange: $selectedRange
)
.navigationTitle("기간 선택")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("닫기") {
dismiss()
}
}
}
}
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}

// MARK: - CalendarView (UIViewRepresentable)

struct CalendarView: UIViewRepresentable {
let startDate: Date
let endDate: Date
@Binding var selectedRange: ClosedRange<Date>?

func makeCoordinator() -> Coordinator {
Coordinator(selectedRange: $selectedRange)
}

func makeUIView(context: Context) -> UICalendarView {
let calendarView = UICalendarView()
calendarView.calendar = Calendar.current
calendarView.locale = Locale.current
calendarView.fontDesign = .rounded

// 날짜 범위 제한
let dateInterval = DateInterval(start: startDate, end: endDate)
calendarView.availableDateRange = dateInterval

// MultiDate selection 설정
let selection = UICalendarSelectionMultiDate(delegate: context.coordinator)
calendarView.selectionBehavior = selection
context.coordinator.selection = selection

// 초기 선택 설정
if let range = selectedRange {
context.coordinator.loadInitialRange(range)
}

return calendarView
}

func updateUIView(_ uiView: UICalendarView, context: Context) {
// selectedRange가 외부에서 변경되면 업데이트
context.coordinator.externalRangeUpdate(selectedRange)
}

// MARK: - Coordinator

class Coordinator: NSObject, UICalendarSelectionMultiDateDelegate {
@Binding var selectedRange: ClosedRange<Date>?
var selection: UICalendarSelectionMultiDate?

private var rangeStart: Date?
private var rangeEnd: Date?
private let calendar = Calendar.current

init(selectedRange: Binding<ClosedRange<Date>?>) {
self._selectedRange = selectedRange
}

// 초기 범위 로드
func loadInitialRange(_ range: ClosedRange<Date>) {
let start = calendar.startOfDay(for: range.lowerBound)
let end = calendar.startOfDay(for: range.upperBound)

rangeStart = start
rangeEnd = end

// 범위 내 모든 날짜 선택
var dates: [DateComponents] = []
var current = start
while current <= end {
let components = calendar.dateComponents([.year, .month, .day], from: current)
dates.append(components)
guard let next = calendar.date(byAdding: .day, value: 1, to: current) else { break }
current = next
}

selection?.setSelectedDates(dates, animated: false)
}

// 외부에서 selectedRange 변경 시
func externalRangeUpdate(_ newRange: ClosedRange<Date>?) {
guard let range = newRange else {
rangeStart = nil
rangeEnd = nil
selection?.setSelectedDates([], animated: false)
return
}

// 현재 내부 상태와 다를 때만 업데이트
if rangeStart != range.lowerBound || rangeEnd != range.upperBound {
loadInitialRange(range)
}
}

// 날짜 선택 가능 여부
func multiDateSelection(_ selection: UICalendarSelectionMultiDate, canSelectDate dateComponents: DateComponents) -> Bool {
return true
}

// 날짜 해제 가능 여부
func multiDateSelection(_ selection: UICalendarSelectionMultiDate, canDeselectDate dateComponents: DateComponents) -> Bool {
return true
}

// 날짜가 선택/해제되었을 때
func multiDateSelection(_ selection: UICalendarSelectionMultiDate, didSelectDate dateComponents: DateComponents) {
guard let selectedDate = calendar.date(from: dateComponents) else { return }
let date = calendar.startOfDay(for: selectedDate)

print("🔵 날짜 선택: \(date)")

// 첫 번째 선택 (start)
if rangeStart == nil {
print(" 📍 첫 번째 선택 (start)")
rangeStart = date
rangeEnd = date
selection.setSelectedDates([dateComponents], animated: false)
updateBinding()
}
// 두 번째 선택 (end)
else if let start = rangeStart, calendar.isDate(start, inSameDayAs: rangeEnd ?? start) {
print(" 📍 두 번째 선택 (end)")
if date < start {
rangeStart = date
rangeEnd = start
} else {
rangeEnd = date
}
fillRange()
updateBinding()
}
// 세 번째 이후 선택 (리셋)
else {
print(" 🔄 세 번째 이후 선택 (리셋)")
rangeStart = date
rangeEnd = date
selection.setSelectedDates([dateComponents], animated: false)
updateBinding()
}
}

func multiDateSelection(_ selection: UICalendarSelectionMultiDate, didDeselectDate dateComponents: DateComponents) {
print("❌ 날짜 해제")
// 날짜 해제 시 전체 리셋
rangeStart = nil
rangeEnd = nil
selection.setSelectedDates([], animated: false)
selectedRange = nil
}

// 범위 채우기
private func fillRange() {
guard let start = rangeStart, let end = rangeEnd else { return }

var dates: [DateComponents] = []
var current = start
while current <= end {
let components = calendar.dateComponents([.year, .month, .day], from: current)
dates.append(components)
guard let next = calendar.date(byAdding: .day, value: 1, to: current) else { break }
current = next
}

selection?.setSelectedDates(dates, animated: false)
print(" 🟢 범위 채우기 완료: \(dates.count)개 날짜")
}

// Binding 업데이트
private func updateBinding() {
guard let start = rangeStart, let end = rangeEnd else {
selectedRange = nil
print(" 🔴 selectedRange -> nil")
return
}

selectedRange = start...end
print(" 🟢 selectedRange 업데이트: \(start) ~ \(end)")
}
}
}
7 changes: 3 additions & 4 deletions Features/ExpenseList/Sources/Components/ExpenseCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,13 @@ public struct ExpenseCardView: View {
.foregroundStyle(Color.gray7)
}
.padding(16)
.background(Color.white)
.background(Color.primary50)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.25), radius: 5, x: 0, y: 4)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.gray.opacity(0.1), lineWidth: 1)
.stroke(Color.gray1, lineWidth: 1)
)
.padding(.horizontal, 16)
.padding(.horizontal, 20)
}
}

Expand Down
Loading
Loading