mycode/myApp/Expiranner/IOS/Theme.swift
songyc macbook 6c4fe817bd feat(expiranner): 라이트/다크 모드 · 시간 포함 유통기한 · 지난 데이터 정리
- 설정 화면 신규(첫 화면 헤더 톱니 진입): 화면 모드(시스템/라이트/다크) 선택,
  '기간 지난 데이터 삭제'로 만료된 기록 일괄 정리(매장 무관, 확인 후 실행)
- Theme 색상을 라이트/다크 자동 전환 동적 색으로 전환, 기본값은 기존과 동일한 다크
- 유통기한에 시각(시:분) 선택 입력: 날짜 선택 후 '시간 추가' 버튼으로만 노출돼
  기본 사용성(사진→날짜→저장)은 그대로 유지, 유제품 등 필요 시에만 사용
- ProductRecord.hasTime 플래그 추가(기본 false, 라이트웨이트 마이그레이션),
  시각 포함 기록은 날짜 상세 그리드에 시각 배지 표시
- .gitignore에 Xcode 빌드 산출물/파생 데이터/SwiftPM 상태 추가해 상태 오염 방지

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2HQuA6RPtkh45QTMN83i2
2026-07-23 17:46:12 +09:00

188 lines
6.7 KiB
Swift

import SwiftUI
import UIKit
// MARK: - (/ )
extension Color {
init(hex: UInt32) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xFF) / 255,
green: Double((hex >> 8) & 0xFF) / 255,
blue: Double(hex & 0xFF) / 255
)
}
/// / .
/// `.preferredColorScheme` .
static func dynamic(light: Color, dark: Color) -> Color {
Color(UIColor { trait in
trait.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
})
}
}
enum Theme {
static let bg = Color.dynamic(light: Color(hex: 0xF2F4F7), dark: Color(hex: 0x0B0E13))
static let surface = Color.dynamic(light: Color(hex: 0xFFFFFF), dark: Color(hex: 0x161B25))
static let surfaceHi = Color.dynamic(light: Color(hex: 0xE9EDF3), dark: Color(hex: 0x202836))
static let stroke = Color.dynamic(light: .black.opacity(0.08), dark: .white.opacity(0.07))
// / .
static let accent = Color.dynamic(light: Color(hex: 0x0FA06E), dark: Color(hex: 0x3BE39F))
static let accentSoft = Color.dynamic(
light: Color(hex: 0x0FA06E).opacity(0.12),
dark: Color(hex: 0x3BE39F).opacity(0.14)
)
// accentGradient ( ).
static let onAccent = Color(hex: 0x07281B)
static let danger = Color.dynamic(light: Color(hex: 0xD64545), dark: Color(hex: 0xFF7A7A))
static let warn = Color.dynamic(light: Color(hex: 0xB77E00), dark: Color(hex: 0xFFC94D))
static let sunday = Color.dynamic(light: Color(hex: 0xD64C42), dark: Color(hex: 0xFF8A80))
static let saturday = Color.dynamic(light: Color(hex: 0x3568C4), dark: Color(hex: 0x7EB6FF))
static let textPrimary = Color.dynamic(light: Color(hex: 0x1A1E26), dark: Color(hex: 0xF3F6FA))
static let textSecondary = Color.dynamic(light: Color(hex: 0x687285), dark: Color(hex: 0x8D99AD))
// ( onAccent ).
static let accentGradient = LinearGradient(
colors: [Color(hex: 0x5CF0B6), Color(hex: 0x1FC98B)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
// MARK: -
/// . `@AppStorage(appearanceStorageKey)` raw .
enum AppearanceMode: Int, CaseIterable, Identifiable {
case system = 0
case light = 1
case dark = 2
var id: Int { rawValue }
var title: String {
switch self {
case .system: return "시스템"
case .light: return "라이트"
case .dark: return "다크"
}
}
var icon: String {
switch self {
case .system: return "iphone"
case .light: return "sun.max.fill"
case .dark: return "moon.fill"
}
}
var colorScheme: ColorScheme? {
switch self {
case .system: return nil
case .light: return .light
case .dark: return .dark
}
}
}
/// . `.dark`.
let appearanceStorageKey = "appearanceMode"
// MARK: -
struct CardBackground: ViewModifier {
var cornerRadius: CGFloat = 18
func body(content: Content) -> some View {
content
.background(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(Theme.surface)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.strokeBorder(Theme.stroke, lineWidth: 1)
)
)
}
}
extension View {
func cardStyle(cornerRadius: CGFloat = 18) -> some View {
modifier(CardBackground(cornerRadius: cornerRadius))
}
}
// MARK: -
enum Haptics {
static func success() { UINotificationFeedbackGenerator().notificationOccurred(.success) }
static func warning() { UINotificationFeedbackGenerator().notificationOccurred(.warning) }
static func tap() { UIImpactFeedbackGenerator(style: .light).impactOccurred() }
static func shutter() { UIImpactFeedbackGenerator(style: .medium).impactOccurred() }
}
// MARK: -
enum KoreanCalendar {
static let calendar: Calendar = {
var c = Calendar(identifier: .gregorian)
c.locale = Locale(identifier: "ko_KR")
return c
}()
static let weekdaySymbols = ["", "", "", "", "", "", ""]
/// . nil, 1....
static func gridDays(year: Int, month: Int) -> [Int?] {
let comps = DateComponents(year: year, month: month, day: 1)
guard let first = calendar.date(from: comps),
let range = calendar.range(of: .day, in: .month, for: first) else { return [] }
let firstWeekday = calendar.component(.weekday, from: first)
let leading = (firstWeekday - calendar.firstWeekday + 7) % 7
return Array(repeating: nil, count: leading) + range.map { $0 }
}
static func date(year: Int, month: Int, day: Int) -> Date? {
calendar.date(from: DateComponents(year: year, month: month, day: day))
}
static func isToday(year: Int, month: Int, day: Int) -> Bool {
let t = calendar.dateComponents([.year, .month, .day], from: .now)
return t.year == year && t.month == month && t.day == day
}
/// ( )
static func daysFromToday(to date: Date) -> Int {
let today = calendar.startOfDay(for: .now)
let target = calendar.startOfDay(for: date)
return calendar.dateComponents([.day], from: today, to: target).day ?? 0
}
static func weekdayColor(columnIndex: Int) -> Color {
switch columnIndex {
case 0: return Theme.sunday
case 6: return Theme.saturday
default: return Theme.textSecondary
}
}
}
// MARK: -
extension UIImage {
func downscaled(maxDimension: CGFloat) -> UIImage {
let longest = max(size.width, size.height)
guard longest > maxDimension, longest > 0 else { return self }
let ratio = maxDimension / longest
let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
let format = UIGraphicsImageRendererFormat.default()
format.scale = 1
return UIGraphicsImageRenderer(size: newSize, format: format).image { _ in
draw(in: CGRect(origin: .zero, size: newSize))
}
}
}