Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0216ef3
fix: X버튼과 추가 버튼을 탭해도 창이 내려가지 않는 현상 해결
opficdev Feb 1, 2026
1da8a12
refactor: 뷰 코드 정리
opficdev Feb 1, 2026
8903b77
refactor: title 문자열 수정
opficdev Feb 1, 2026
0439400
feat: 여백 부분을 탭하면 설명 필드에 키보드 포커싱이 되도록 추가
opficdev Feb 1, 2026
d9ab135
ui: 디자인 수정
opficdev Feb 2, 2026
778b038
style: 띄어쓰기 추가
opficdev Feb 5, 2026
bc35a44
refactor: adaptiveButtonStyle 수정
opficdev Feb 5, 2026
66b4a51
feat: 좌측정렬 레이아웃 구현
opficdev Feb 5, 2026
904fe43
feat: 태그 컴포넌트 구현
opficdev Feb 5, 2026
0533939
refactor: 동일한 태그명일 시 추가되지 않도록 개선
opficdev Feb 6, 2026
bafac89
feat: 마감일 피커 구현
opficdev Feb 6, 2026
1d4791f
chore: Tag 관련 컴포넌트 분리
opficdev Feb 6, 2026
058adc9
refactor: 태그 입력 및 확인을 sheet 버전으로 변경
opficdev Feb 7, 2026
b1bb314
style: Preview 제거
opficdev Feb 7, 2026
d1cb6e4
refactor: focus 변수 제거
opficdev Feb 7, 2026
5f35483
feat: 시트가 내려갈 때 입력했던 문자열 초기화
opficdev Feb 7, 2026
6bcfacc
feat: spacing 변수로 일관화
opficdev Feb 7, 2026
78df44f
style: 코드 위치 이동 및 주석 제거
opficdev Feb 7, 2026
7f16c1e
feat: 추가할 태그 문자열이 없거나 포함되어 있다면 탭 이펙트 차단
opficdev Feb 7, 2026
20fcc64
feat: 마감일 체크박스 추가
opficdev Feb 7, 2026
39aaedf
refactor: 불필요 ignoresSafeArea 제거
opficdev Feb 7, 2026
b3aafa6
fix: 스크롤 시 툴바 배경에 내용과 header과 어우러지지 않아 수정
opficdev Feb 7, 2026
92f1cdb
fix: Todo 작성 시 kind가 etc로만 적용되는 현상 해결
opficdev Feb 7, 2026
fe0d240
fix: DatePicker 높이보다 sheet의 safeArea로 인해 Picker이 가려지는 현상 해결
opficdev Feb 7, 2026
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
24 changes: 0 additions & 24 deletions DevLog/Presentation/Extension/View+.swift

This file was deleted.

59 changes: 34 additions & 25 deletions DevLog/Presentation/ViewModel/TodoEditorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
//

import Foundation
import OrderedCollections

final class TodoEditorViewModel: Store {
struct State {
var title: String = ""
var content: String = ""
var dueDate: Date?
var tags: [String] = []
var tags: OrderedSet<String> = []
var tagText: String = ""
var focusOnEditor: Bool = false
var hasDueDate: Bool { return dueDate != nil }
Expand All @@ -28,10 +29,10 @@ final class TodoEditorViewModel: Store {
}

enum Action {
case addTag
case addTag(String)
case removeTag(String)
case setContent(String)
case setDueDate(Date)
case setDueDate(Date?)
case setTabViewTag(Tag)
case setTagText(String)
case setTitle(String)
Expand All @@ -50,39 +51,47 @@ final class TodoEditorViewModel: Store {
private let createdAt: Date?
private let kind: TodoKind

init(title: String, todo: Todo? = nil) {
self.navigationTitle = title
self.id = todo?.id ?? UUID().uuidString
self.isPinned = todo?.isPinned ?? false
self.isCompleted = todo?.isCompleted ?? false
self.isChecked = todo?.isChecked ?? false
self.createdAt = todo?.createdAt ?? nil
self.kind = todo?.kind ?? .etc
if let todo {
state.title = todo.title
state.content = todo.content
state.dueDate = todo.dueDate
state.tags = todo.tags
}
// 새로운 Todo 생성용 생성자
init(kind: TodoKind) {
self.navigationTitle = "새 \(kind.localizedName) 추가"
self.id = UUID().uuidString
self.isPinned = false
self.isCompleted = false
self.isChecked = false
self.createdAt = nil
self.kind = kind
}

// 기존 Todo 편집용 생성자
init(todo: Todo) {
self.navigationTitle = "편집"
self.id = todo.id
self.isPinned = todo.isPinned
self.isCompleted = todo.isCompleted
self.isChecked = todo.isChecked
self.createdAt = todo.createdAt
self.kind = todo.kind
state.title = todo.title
state.content = todo.content
state.dueDate = todo.dueDate
state.tags = OrderedSet(todo.tags)
}

func reduce(with action: Action) -> [SideEffect] {
switch action {
case .addTag:
let tagText = state.tagText
if !state.tags.contains(tagText) && !tagText.isEmpty {
state.tags.append(tagText)
state.tagText = ""
}
case .addTag(let tag):
if !tag.isEmpty { state.tags.append(tag) }
case .removeTag(let tagText):
state.tags.removeAll { $0 == tagText }
case .setContent(let stringValue),
.setTagText(let stringValue),
.setTitle(let stringValue):
handleStringAction(action, stringValue: stringValue)
case .setDueDate(let dueDate):
if let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: Date()) {
if let tomorrowDate = calendar.date(byAdding: .day, value: 1, to: Date()), let dueDate {
state.dueDate = max(dueDate, tomorrowDate)
} else {
state.dueDate = nil
}
case .setTabViewTag(let tag):
state.tabViewTag = tag
Expand Down Expand Up @@ -125,7 +134,7 @@ extension TodoEditorViewModel {
createdAt: self.createdAt ?? date,
updatedAt: date,
dueDate: state.dueDate,
tags: state.tags,
tags: state.tags.map { $0 },
kind: self.kind
)
}
Expand Down
9 changes: 6 additions & 3 deletions DevLog/Resource/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,6 @@
},
"계정 연동" : {

},
"내용을 입력하세요" : {

},
"네트워크 문제" : {

Expand Down Expand Up @@ -289,6 +286,9 @@
},
"생성" : {

},
"설명(선택 사항)" : {

},
"설정" : {

Expand Down Expand Up @@ -352,6 +352,9 @@
},
"태그" : {

},
"태그 입력" : {

},
"테마" : {

Expand Down
142 changes: 142 additions & 0 deletions DevLog/UI/Common/Componeent/Tag+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//
// Tag+.swift
// DevLog
//
// Created by 최윤진 on 2/6/26.
//

import SwiftUI

struct Tag: View {
@Environment(\.colorScheme) private var colorScheme
@State private var height: CGFloat = 0
private let name: String
private let isEditing: Bool
private var action: (() -> Void)?

init(_ name: String, isEditing: Bool, action: (() -> Void)? = nil) {
self.name = name
self.isEditing = isEditing
self.action = action
}

var body: some View {
HStack(spacing: 4) {
Text(name)
.foregroundStyle(.blue)
.bold()
.lineLimit(1)
.fixedSize()
.padding(.vertical, 4)
.padding(.leading, 8)
.padding(.trailing, isEditing ? 0 : 8)
.background {
GeometryReader { geo in
Color.clear
.onAppear {
height = geo.size.height
}
}
}

if isEditing {
Button {
action?()
} label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: height, height: height)
.symbolRenderingMode(.palette)
.foregroundStyle(
.blue,
.black.opacity(colorScheme == .light ? 0 : 0.4)
)

}
}
}
.background {
Capsule()
.fill(.blue.opacity(0.2))
}
}
}

struct TagLayout: Layout {
var verticalSpacing: CGFloat = 8
var horizontalSpacing: CGFloat = 8

func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
let maxWidth = proposal.width ?? .infinity
let rows = computeRows(maxWidth: maxWidth, subviews: subviews)
let height =
rows.reduce(0) { $0 + $1.maxHeight }
+ CGFloat(max(0, rows.count - 1)) * verticalSpacing
return CGSize(width: proposal.width ?? 0, height: height)
}

func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let rows = computeRows(maxWidth: bounds.width, subviews: subviews)
var minY = bounds.minY

for row in rows {
var minX = bounds.minX

for index in row.indices {
let size = subviews[index].sizeThatFits(.unspecified)
subviews[index].place(
at: CGPoint(x: minX, y: minY),
proposal: ProposedViewSize(size)
)
minX += size.width + horizontalSpacing
}

minY += row.maxHeight + verticalSpacing
}
}

private func computeRows(
maxWidth: CGFloat,
subviews: Subviews
) -> [Row] {
let availableWidth = maxWidth > 0 ? maxWidth : .infinity
var rows: [Row] = []
var currentRow = Row()
var currentWidth: CGFloat = 0

for (index, subview) in subviews.enumerated() {
let size = subview.sizeThatFits(.unspecified)

if currentWidth + size.width > availableWidth && !currentRow.indices.isEmpty {
rows.append(currentRow)
currentRow = Row()
currentWidth = 0
}

currentRow.indices.append(index)
currentRow.maxHeight = max(currentRow.maxHeight, size.height)
currentWidth += size.width + horizontalSpacing
}

if !currentRow.indices.isEmpty {
rows.append(currentRow)
}

return rows
}

private struct Row {
var indices: [Int] = []
var maxHeight: CGFloat = 0
}
}
31 changes: 31 additions & 0 deletions DevLog/UI/Extension/EnvironmentValues+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// EnvironmentValues+.swift
// DevLog
//
// Created by 최윤진 on 2/6/26.
//

import SwiftUI

extension EnvironmentValues {

var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}

private struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
return EdgeInsets()
}
return window.safeAreaInsets.insets
}
}
}

extension UIEdgeInsets {
var insets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}
56 changes: 56 additions & 0 deletions DevLog/UI/Extension/View+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// View+.swift
// DevLog
//
// Created by 최윤진 on 11/22/25.
//

import SwiftUI

extension View {
var sceneWidth: CGFloat {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
else { return UIScreen.main.bounds.width }

return windowScene.screen.bounds.width
}

var sceneHeight: CGFloat {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
else { return UIScreen.main.bounds.height }

return windowScene.screen.bounds.height
}

var safeAreaInsets: UIEdgeInsets {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first
else { return UIEdgeInsets.zero }

return window.safeAreaInsets
}

@ViewBuilder
func adaptiveButtonStyle(_ color: Color? = nil) -> some View {
if #available(iOS 26.0, *), color == nil {
self.buttonStyle(.glass)
} else {
self.foregroundStyle(Color(.label))
.font(.footnote)
.padding(.vertical, 8)
.padding(.horizontal, 16)
.background {
Capsule()
.fill(.ultraThinMaterial)
.background {
Capsule()
.fill(color ?? Color.clear)
}
.overlay {
Capsule()
.stroke(Color.white.opacity(0.2), lineWidth: 1)
}
}
}
}
}
Loading