Skip to content
Open
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Select Xcode 16.1
run: sudo xcode-select -s /Applications/Xcode_16.1.app
- name: Select Xcode 16.4
run: sudo xcode-select -s /Applications/Xcode_16.4.app

- name: Install SwiftGen
run: brew install swiftgen
- name: Install SwiftGen if needed
run: command -v swiftgen || brew install swiftgen

- name: Setup Ruby
uses: ruby/setup-ruby@v1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ jobs:
- name: Select Xcode 16.1
run: sudo xcode-select -s /Applications/Xcode_16.1.app

- name: Install SwiftGen
run: brew install swiftgen
- name: Install SwiftGen if needed
run: command -v swiftgen || brew install swiftgen

- name: Setup Ruby
uses: ruby/setup-ruby@v1
Expand Down
18 changes: 0 additions & 18 deletions TimerStamp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@
B8B32C182DB13B2600CD65B3 /* Frameworks */,
B8B32C192DB13B2600CD65B3 /* Resources */,
B82186412DCDED46002B370E /* Embed Foundation Extensions */,
B80DB4F72DFAFDFA008BFBA2 /* ShellScript */,
B86F23C62E3BAE6300320FE4 /* ShellScript */,
);
buildRules = (
Expand Down Expand Up @@ -327,23 +326,6 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
B80DB4F72DFAFDFA008BFBA2 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 12;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nif [ \"${ACTION}\" = \"archive\" ]; then\n buildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\")\n buildNumber=$(($buildNumber + 1))\n /usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n echo \"Updated build number to $buildNumber\"\nfi\n";
};
B86F23C62E3BAE6300320FE4 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down
21 changes: 21 additions & 0 deletions TimerStamp/Domains/Services/ImageSaveService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// ImageSaveService.swift
// TimerStamp
//

import UIKit

final class ImageSaveService: NSObject {
static let shared = ImageSaveService()

@objc func didFinishSaving(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
DispatchQueue.main.async {
let success = (error == nil)
NotificationCenter.default.post(name: .imageSaveCompleted, object: success)
}
}
}

extension Notification.Name {
static let imageSaveCompleted = Notification.Name("imageSaveCompleted")
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// PhotoSelectionViewMdoel.swift
// PhotoSelectionViewModel.swift
// TimerStamp
//
// Created by 이예슬 on 5/6/25.
Expand All @@ -17,27 +17,27 @@ class PhotoSelectionViewModel: ObservableObject {
@Published var isShowingImagePicker = false
@Published var isShowingModal = false
@Published var isShowingSourceDialog = false
@Published var sourceType: UIImagePickerController.SourceType = .photoLibrary


@Published private(set) var selectedSourceType: PhotoSourceType = .photoLibrary

/// 인증사진 플로우 완료(모달 닫기) 시 호출됩니다.
/// TimerViewModel.reset 등을 주입해 View 레이어의 협력 로직을 제거합니다.
var onCertificationCompleted: (() -> Void)?

func didSelectImage(_ image: UIImage?) {
selectedImage = image
if image != nil {
isShowingModal = true
}
}

func dismissModal() {
isShowingModal = false
selectedImage = nil
onCertificationCompleted?()
}

func selectSource(_ source: PhotoSourceType) {
switch source {
case .camera:
sourceType = .camera
case .photoLibrary:
sourceType = .photoLibrary
}
selectedSourceType = source
isShowingImagePicker = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// CertificationImageRenderer.swift
// TimerStamp
//

import SwiftUI
import UIKit

enum CertificationImageRenderer {
@MainActor
static func render(image: UIImage, minutes: Int) -> UIImage? {
let view = ZStack {
GeometryReader { geo in
let timerSize = 400.0
let radius = timerSize / 3.4
let margin: CGFloat = 60

// 1. 원본 이미지
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)

// 2. 그라데이션 오버레이
LinearGradient(
gradient: Gradient(colors: [
Color.black.opacity(0),
Color.black.opacity(0.3)
]),
startPoint: UnitPoint(x: 0.5, y: 0.7),
endPoint: .bottom
)
.frame(width: geo.size.width, height: geo.size.height)

// 3. 절대 위치로 하단 정렬
ZStack(alignment: .bottom) {
Color.clear
.frame(width: geo.size.width, height: geo.size.height)

HStack(alignment: .bottom, spacing: 0) {
// 좌측: 타이머
VStack(alignment: .leading) {
SimpleTimerView(minutes: minutes, radius: radius)
.opacity(0.74)
.frame(maxHeight: .infinity, alignment: .bottom)
.padding(66)
}

Spacer(minLength: 20)

VStack(alignment: .trailing) {
Text("✨ \(L10n.focusComplete(minutes))")
.font(.system(size: 66, weight: .heavy))
.fontWidth(.expanded)
.kerning(-2)
.foregroundColor(.white)
.multilineTextAlignment(.trailing)
.lineLimit(nil)
.padding(.bottom, 6)

Text(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))
.font(.system(size: 58, weight: .light))
.foregroundColor(.white.opacity(0.95))
.padding(.bottom, 10)
}
.frame(maxHeight: .infinity, alignment: .bottom)
}
.padding(.horizontal, margin)
.padding(.bottom, margin)
}
}
.frame(width: 1080, height: 1920)
}
.background(Color.clear)

let renderer = ImageRenderer(content: view)
renderer.proposedSize = .init(CGSize(width: 1080, height: 1920))
renderer.isOpaque = true
return renderer.uiImage
}
}
Loading
Loading