From 0216ef3ef7156d1221569b1e5b578a84151453f1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 21:37:17 +0900 Subject: [PATCH 01/24] =?UTF-8?q?fix:=20X=EB=B2=84=ED=8A=BC=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B2=84=ED=8A=BC=EC=9D=84=20=ED=83=AD?= =?UTF-8?q?=ED=95=B4=EB=8F=84=20=EC=B0=BD=EC=9D=B4=20=EB=82=B4=EB=A0=A4?= =?UTF-8?q?=EA=B0=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=98=84=EC=83=81=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index 825d2e6..caa85b7 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -96,7 +96,7 @@ struct TodoView: View { .navigationBarTitleDisplayMode(.large) .fullScreenCover(isPresented: Binding( get: { viewModel.state.showEditor }, - set: { _, _ in viewModel.send(.openEditor) }) + set: { _, _ in viewModel.send(.closeEditor) }) ) { let title = "새 \(viewModel.state.kind.localizedName)" TodoEditorView( From 1da8a125a3d03dd2ff809961ef3356ca730b7217 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 22:27:56 +0900 Subject: [PATCH 02/24] =?UTF-8?q?refactor:=20=EB=B7=B0=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 301 +++++++++++++++------------- 1 file changed, 167 insertions(+), 134 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 8c45a0b..22fa294 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -17,157 +17,190 @@ struct TodoEditorView: View { var body: some View { NavigationStack { ScrollView { - VStack(spacing: 10) { - TextField("", text: Binding( - get: { viewModel.state.title }, - set: { viewModel.send(.setTitle($0)) } - ), - prompt: Text("제목").foregroundColor(Color.gray) + LazyVStack(spacing: 10) { + titleField + Divider() + dueDateSelector + Divider() + tagField + LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { + Section { + tabView + } header: { + tabViewSelector + } + } + .border(Color.blue) + } + } + .navigationTitle(viewModel.navigationTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolBar } + } + } + + private var titleField: some View { + TextField("", text: Binding( + get: { viewModel.state.title }, + set: { viewModel.send(.setTitle($0)) } + ), + prompt: Text("제목").foregroundColor(Color.gray) + ) + .font(.title3) + .padding(.horizontal) + } + + private var dueDateSelector: some View { + HStack { + if let dueDate = viewModel.state.dueDate { + HStack { + DatePicker( + "마감일", + selection: Binding( + get: { dueDate }, + set: { viewModel.send(.setDueDate($0)) } + ), + displayedComponents: .date ) - .font(.title3) - .padding(.horizontal) + .datePickerStyle(.compact) + .foregroundStyle(viewModel.state.hasDueDate ? Color.primary : Color.secondary) Divider() - if let dueDate = viewModel.state.dueDate { + Button(action: { + viewModel.send(.toggleDueDate) + }) { + CheckBox(isChecked: viewModel.state.hasDueDate) + } + } + .padding(.horizontal) + } else { + + } + } + } + + private var tagField: some View { + HStack { + Text("태그") + .foregroundStyle(viewModel.state.tags.isEmpty ? Color.secondary : Color.primary) + Divider() + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(viewModel.state.tags, id: \.self) { tag in HStack { - DatePicker("마감일", - selection: Binding( - get: { dueDate }, - set: { viewModel.send(.setDueDate($0)) } - ), - displayedComponents: .date) - .datePickerStyle(.compact) - .foregroundStyle(viewModel.state.hasDueDate ? Color.primary : Color.secondary) - Divider() + Text(tag) Button(action: { - viewModel.send(.toggleDueDate) + viewModel.send(.removeTag(tag)) }) { - CheckBox(isChecked: viewModel.state.hasDueDate) + Image(systemName: "xmark") + .font(.caption) + .foregroundStyle(Color.gray) } } - .padding(.horizontal) + .padding(.horizontal, 8) + .background( + Capsule() + .fill(Color(UIColor.systemFill)) + ) } - Divider() - HStack { - Text("태그") - .foregroundStyle(viewModel.state.tags.isEmpty ? Color.secondary : Color.primary) - Divider() - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(viewModel.state.tags, id: \.self) { tag in - HStack { - Text(tag) - Button(action: { - viewModel.send(.removeTag(tag)) - }) { - Image(systemName: "xmark") - .font(.caption) - .foregroundStyle(Color.gray) - } - } - .padding(.horizontal, 8) - .background( - Capsule() - .fill(Color(UIColor.systemFill)) - ) - } - TextField("", - text: Binding( - get: { viewModel.state.tagText }, - set: { viewModel.send(.setTagText($0)) } - )) - .focused($focusOnTagField) - .onSubmit { - viewModel.send(.addTag) - } - .onChange(of: focusOnTagField) { focused in - if !focused { - viewModel.send(.addTag) - } - } - } - } - Divider() - Button(action: { - focusOnTagField.toggle() - }) { - Image(systemName: "\(focusOnTagField ? "xmark" : "plus").circle.fill") - .foregroundStyle(Color.gray) - .font(.title2) - } + TextField( + "", + text: Binding( + get: { viewModel.state.tagText }, + set: { viewModel.send(.setTagText($0)) } + ) + ) + .focused($focusOnTagField) + .onSubmit { + viewModel.send(.addTag) } - .padding(.horizontal) - } - LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { - Section { - Group { - if viewModel.state.tabViewTag == .editor { - TextField( - "", - text: Binding( - get: { viewModel.state.content }, - set: { viewModel.send(.setContent($0)) } - ), - prompt: Text("내용을 입력하세요"), - axis: .vertical - ) - } else { - Markdown(viewModel.state.content) - .markdownTheme(.basic) - } - } - .padding(.horizontal) - } header: { - VStack(spacing: 0) { - Divider() - HStack(spacing: 0) { - Button(action: { - viewModel.send(.setTabViewTag(.editor)) - }) { - Text("편집") - .frame(maxWidth: .infinity) - .foregroundStyle( - viewModel.state.tabViewTag == .editor ? Color.primary : Color.secondary - ) - } - Divider() - Button(action: { - viewModel.send(.setTabViewTag(.preview)) - }) { - Text("미리보기") - .frame(maxWidth: .infinity) - .foregroundStyle( - viewModel.state.tabViewTag == .preview ? Color.primary : Color.gray - ) - } - } - .padding(.vertical, 10) - .background(Color(UIColor.systemBackground)) - Divider() + .onChange(of: focusOnTagField) { focused in + if !focused { + viewModel.send(.addTag) } } } } - .navigationTitle(viewModel.navigationTitle) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button(action: { - dismiss() - }) { - Image(systemName: "xmark")} - .bold() + Divider() + Button(action: { + focusOnTagField.toggle() + }) { + Image(systemName: "\(focusOnTagField ? "xmark" : "plus").circle.fill") + .foregroundStyle(Color.gray) + .font(.title2) + } + } + .padding(.horizontal) + } + + private var tabViewSelector: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 0) { + Button(action: { + viewModel.send(.setTabViewTag(.editor)) + }) { + Text("편집") + .frame(maxWidth: .infinity) + .foregroundStyle( + viewModel.state.tabViewTag == .editor ? Color.primary : Color.secondary + ) } - ToolbarItem(placement: .topBarTrailing) { - Button(action: { - onSubmit?(viewModel.upsertTodo()) - dismiss() - }) { - Text("추가") - } - .disabled(!viewModel.state.isValidToSave) + Divider() + Button(action: { + viewModel.send(.setTabViewTag(.preview)) + }) { + Text("미리보기") + .frame(maxWidth: .infinity) + .foregroundStyle( + viewModel.state.tabViewTag == .preview ? Color.primary : Color.gray + ) } } + .padding(.vertical, 10) + .background(Color(UIColor.systemBackground)) + Divider() + } + } + + private var tabView: some View { + Group { + if viewModel.state.tabViewTag == .editor { + TextField( + "", + text: Binding( + get: { viewModel.state.content }, + set: { viewModel.send(.setContent($0)) } + ), + prompt: Text("내용을 입력하세요"), + axis: .vertical + ) + } else { + Markdown(viewModel.state.content) + .markdownTheme(.basic) + } + } + .padding(.horizontal) + .padding(.top, 10) + } + + @ToolbarContentBuilder + private var toolBar: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + Button(action: { + dismiss() + }) { + Image(systemName: "xmark")} + .bold() + } + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + onSubmit?(viewModel.upsertTodo()) + dismiss() + }) { + Text("추가") + } + .disabled(!viewModel.state.isValidToSave) } } } From 8903b778deb632fc1ebbba1ed6e71445a9a9f4d1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 23:00:41 +0900 Subject: [PATCH 03/24] =?UTF-8?q?refactor:=20title=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/ViewModel/TodoEditorViewModel.swift | 2 +- DevLog/UI/Home/TodoView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index 5e3722d..60fd638 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -51,7 +51,7 @@ final class TodoEditorViewModel: Store { private let kind: TodoKind init(title: String, todo: Todo? = nil) { - self.navigationTitle = title + self.navigationTitle = "새 \(title)" self.id = todo?.id ?? UUID().uuidString self.isPinned = todo?.isPinned ?? false self.isCompleted = todo?.isCompleted ?? false diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index caa85b7..49350da 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -98,7 +98,7 @@ struct TodoView: View { get: { viewModel.state.showEditor }, set: { _, _ in viewModel.send(.closeEditor) }) ) { - let title = "새 \(viewModel.state.kind.localizedName)" + let title = viewModel.state.kind.localizedName TodoEditorView( viewModel: TodoEditorViewModel(title: title), onSubmit: { viewModel.send(.upsertTodo($0)) } From 04394000b970e32a98c4f08dda0b8d85c2c57497 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 1 Feb 2026 23:16:41 +0900 Subject: [PATCH 04/24] =?UTF-8?q?feat:=20=EC=97=AC=EB=B0=B1=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=9D=84=20=ED=83=AD=ED=95=98=EB=A9=B4=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=ED=95=84=EB=93=9C=EC=97=90=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=ED=8F=AC=EC=BB=A4=EC=8B=B1=EC=9D=B4=20=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 62 +++++++++++++++-------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 22fa294..81fd243 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -11,26 +11,30 @@ import MarkdownUI struct TodoEditorView: View { @StateObject var viewModel: TodoEditorViewModel @Environment(\.dismiss) private var dismiss - @FocusState var focusOnTagField: Bool + @FocusState var focusOnContentField: Bool var onSubmit: ((Todo) -> Void)? var body: some View { NavigationStack { - ScrollView { - LazyVStack(spacing: 10) { - titleField - Divider() - dueDateSelector - Divider() - tagField - LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { - Section { - tabView - } header: { - tabViewSelector + ZStack { + ScrollView { + LazyVStack(spacing: 10) { + titleField + LazyVStack( + alignment: .leading, + spacing: 0, + pinnedViews: [.sectionHeaders] + ) { + Section { + tabView + } header: { + tabViewSelector + } } } - .border(Color.blue) + } + .onTapGesture { + focusOnContentField = true } } .navigationTitle(viewModel.navigationTitle) @@ -84,7 +88,7 @@ struct TodoEditorView: View { .foregroundStyle(viewModel.state.tags.isEmpty ? Color.secondary : Color.primary) Divider() ScrollView(.horizontal, showsIndicators: false) { - HStack { + LazyHStack { ForEach(viewModel.state.tags, id: \.self) { tag in HStack { Text(tag) @@ -93,7 +97,6 @@ struct TodoEditorView: View { }) { Image(systemName: "xmark") .font(.caption) - .foregroundStyle(Color.gray) } } .padding(.horizontal, 8) @@ -110,24 +113,26 @@ struct TodoEditorView: View { set: { viewModel.send(.setTagText($0)) } ) ) - .focused($focusOnTagField) +// .focused($focusOnTagField) .onSubmit { viewModel.send(.addTag) } - .onChange(of: focusOnTagField) { focused in - if !focused { - viewModel.send(.addTag) - } - } +// .onChange(of: focusOnTagField) { focused in +// if !focused { +// viewModel.send(.addTag) +// } +// } } } Divider() Button(action: { - focusOnTagField.toggle() +// focusOnTagField.toggle() +// if focusOnTagField { +// focusOnContentField = false +// } }) { - Image(systemName: "\(focusOnTagField ? "xmark" : "plus").circle.fill") - .foregroundStyle(Color.gray) - .font(.title2) +// Image(systemName: "\(focusOnTagField ? "xmark" : "plus").circle.fill") +// .font(.title2) } } .padding(.horizontal) @@ -135,7 +140,6 @@ struct TodoEditorView: View { private var tabViewSelector: some View { VStack(spacing: 0) { - Divider() HStack(spacing: 0) { Button(action: { viewModel.send(.setTabViewTag(.editor)) @@ -159,7 +163,6 @@ struct TodoEditorView: View { } .padding(.vertical, 10) .background(Color(UIColor.systemBackground)) - Divider() } } @@ -172,9 +175,10 @@ struct TodoEditorView: View { get: { viewModel.state.content }, set: { viewModel.send(.setContent($0)) } ), - prompt: Text("내용을 입력하세요"), + prompt: Text("설명(선택 사항)").font(.callout), axis: .vertical ) + .focused($focusOnContentField) } else { Markdown(viewModel.state.content) .markdownTheme(.basic) From d9ab135c97fba327da8f6d3d73554ee0c3abc1c5 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 2 Feb 2026 09:45:48 +0900 Subject: [PATCH 05/24] =?UTF-8?q?ui:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/Presentation/Extension/View+.swift | 24 ---- DevLog/Resource/Localizable.xcstrings | 6 +- DevLog/UI/Extension/View+.swift | 63 ++++++++++ DevLog/UI/Home/TodoEditorView.swift | 141 ++++++++-------------- 4 files changed, 113 insertions(+), 121 deletions(-) delete mode 100644 DevLog/Presentation/Extension/View+.swift create mode 100644 DevLog/UI/Extension/View+.swift diff --git a/DevLog/Presentation/Extension/View+.swift b/DevLog/Presentation/Extension/View+.swift deleted file mode 100644 index c3c9782..0000000 --- a/DevLog/Presentation/Extension/View+.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 - } -} diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index cc5e3f8..8d198d2 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -250,9 +250,6 @@ }, "계정 연동" : { - }, - "내용을 입력하세요" : { - }, "네트워크 문제" : { @@ -289,6 +286,9 @@ }, "생성" : { + }, + "설명(선택 사항)" : { + }, "설정" : { diff --git a/DevLog/UI/Extension/View+.swift b/DevLog/UI/Extension/View+.swift new file mode 100644 index 0000000..4b08781 --- /dev/null +++ b/DevLog/UI/Extension/View+.swift @@ -0,0 +1,63 @@ +// +// 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() -> some View { + if #available(iOS 26.0, *) { + self.buttonStyle(.glass) + } else if #available(iOS 17.0, *) { + capsuleButton + .background { + Capsule() + .fill(.ultraThinMaterial) + .strokeBorder(.white.opacity(0.2), lineWidth: 1) + } + } else { + capsuleButton + .background { + Capsule() + .fill(.ultraThinMaterial) + .overlay { + Capsule() + .stroke(Color.white.opacity(0.2), lineWidth: 1) + } + } + } + } + + private var capsuleButton: some View { + self.foregroundStyle(Color(.label)) + .font(.footnote) + .padding(.vertical, 8) + .padding(.horizontal, 16) + } +} diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 81fd243..25854af 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -11,12 +11,13 @@ import MarkdownUI struct TodoEditorView: View { @StateObject var viewModel: TodoEditorViewModel @Environment(\.dismiss) private var dismiss - @FocusState var focusOnContentField: Bool + @FocusState private var field: Field? + @State private var showDueDatePicker: Bool = false var onSubmit: ((Todo) -> Void)? var body: some View { NavigationStack { - ZStack { + ZStack(alignment: .bottom) { ScrollView { LazyVStack(spacing: 10) { titleField @@ -34,9 +35,37 @@ struct TodoEditorView: View { } } .onTapGesture { - focusOnContentField = true + field = .description } + HStack { + Button { + field = nil + } label: { + Label { + Text("태그") + } icon: { + Image(systemName: "tag") + .foregroundStyle(.gray) + } + } + .adaptiveButtonStyle() + + Button { + field = nil + showDueDatePicker = true + } label: { + Label { + Text("마감일") + } icon: { + Image(systemName: "calendar") + .foregroundStyle(.gray) + } + } + .adaptiveButtonStyle() + } + .padding(.bottom, 16 + safeAreaInsets.bottom / 4) } + .ignoresSafeArea(.container, edges: .bottom) .navigationTitle(viewModel.navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { toolBar } @@ -44,97 +73,15 @@ struct TodoEditorView: View { } private var titleField: some View { - TextField("", text: Binding( - get: { viewModel.state.title }, - set: { viewModel.send(.setTitle($0)) } - ), - prompt: Text("제목").foregroundColor(Color.gray) + TextField( + "", + text: Binding( + get: { viewModel.state.title }, + set: { viewModel.send(.setTitle($0)) } + ), + prompt: Text("제목").foregroundColor(Color.gray) ) - .font(.title3) - .padding(.horizontal) - } - - private var dueDateSelector: some View { - HStack { - if let dueDate = viewModel.state.dueDate { - HStack { - DatePicker( - "마감일", - selection: Binding( - get: { dueDate }, - set: { viewModel.send(.setDueDate($0)) } - ), - displayedComponents: .date - ) - .datePickerStyle(.compact) - .foregroundStyle(viewModel.state.hasDueDate ? Color.primary : Color.secondary) - Divider() - Button(action: { - viewModel.send(.toggleDueDate) - }) { - CheckBox(isChecked: viewModel.state.hasDueDate) - } - } - .padding(.horizontal) - } else { - - } - } - } - - private var tagField: some View { - HStack { - Text("태그") - .foregroundStyle(viewModel.state.tags.isEmpty ? Color.secondary : Color.primary) - Divider() - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - ForEach(viewModel.state.tags, id: \.self) { tag in - HStack { - Text(tag) - Button(action: { - viewModel.send(.removeTag(tag)) - }) { - Image(systemName: "xmark") - .font(.caption) - } - } - .padding(.horizontal, 8) - .background( - Capsule() - .fill(Color(UIColor.systemFill)) - ) - } - - TextField( - "", - text: Binding( - get: { viewModel.state.tagText }, - set: { viewModel.send(.setTagText($0)) } - ) - ) -// .focused($focusOnTagField) - .onSubmit { - viewModel.send(.addTag) - } -// .onChange(of: focusOnTagField) { focused in -// if !focused { -// viewModel.send(.addTag) -// } -// } - } - } - Divider() - Button(action: { -// focusOnTagField.toggle() -// if focusOnTagField { -// focusOnContentField = false -// } - }) { -// Image(systemName: "\(focusOnTagField ? "xmark" : "plus").circle.fill") -// .font(.title2) - } - } + .focused($field, equals: .title) .padding(.horizontal) } @@ -143,6 +90,7 @@ struct TodoEditorView: View { HStack(spacing: 0) { Button(action: { viewModel.send(.setTabViewTag(.editor)) + field = .description }) { Text("편집") .frame(maxWidth: .infinity) @@ -153,6 +101,7 @@ struct TodoEditorView: View { Divider() Button(action: { viewModel.send(.setTabViewTag(.preview)) + field = nil }) { Text("미리보기") .frame(maxWidth: .infinity) @@ -178,7 +127,7 @@ struct TodoEditorView: View { prompt: Text("설명(선택 사항)").font(.callout), axis: .vertical ) - .focused($focusOnContentField) + .focused($field, equals: .description) } else { Markdown(viewModel.state.content) .markdownTheme(.basic) @@ -207,4 +156,8 @@ struct TodoEditorView: View { .disabled(!viewModel.state.isValidToSave) } } + + private enum Field: Hashable { + case title, description, tag + } } From 778b038648c21113c9433f72024d63bb150d6096 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 5 Feb 2026 23:15:50 +0900 Subject: [PATCH 06/24] =?UTF-8?q?style:=20=EB=9D=84=EC=96=B4=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/HomeView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevLog/UI/Home/HomeView.swift b/DevLog/UI/Home/HomeView.swift index 8f35c29..855031f 100644 --- a/DevLog/UI/Home/HomeView.swift +++ b/DevLog/UI/Home/HomeView.swift @@ -121,7 +121,7 @@ struct HomeView: View { .navigationDestination(for: Path.self) { path in switch path { case .kind(let todoKind): - TodoView(viewModel:TodoViewModel( + TodoView(viewModel: TodoViewModel( fetchTodosByKindUseCase: container.resolve(FetchTodosByKindUseCase.self), upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), kind: todoKind From bc35a44cc2b7209b4de16d819e28e4e02cb77438 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 01:46:36 +0900 Subject: [PATCH 07/24] =?UTF-8?q?refactor:=20adaptiveButtonStyle=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Extension/View+.swift | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/DevLog/UI/Extension/View+.swift b/DevLog/UI/Extension/View+.swift index 4b08781..43cd522 100644 --- a/DevLog/UI/Extension/View+.swift +++ b/DevLog/UI/Extension/View+.swift @@ -34,15 +34,11 @@ extension View { func adaptiveButtonStyle() -> some View { if #available(iOS 26.0, *) { self.buttonStyle(.glass) - } else if #available(iOS 17.0, *) { - capsuleButton - .background { - Capsule() - .fill(.ultraThinMaterial) - .strokeBorder(.white.opacity(0.2), lineWidth: 1) - } } else { - capsuleButton + self.foregroundStyle(Color(.label)) + .font(.footnote) + .padding(.vertical, 8) + .padding(.horizontal, 16) .background { Capsule() .fill(.ultraThinMaterial) @@ -53,11 +49,4 @@ extension View { } } } - - private var capsuleButton: some View { - self.foregroundStyle(Color(.label)) - .font(.footnote) - .padding(.vertical, 8) - .padding(.horizontal, 16) - } } From 66b4a518b181898901008b4f55a1a70d0da7dc66 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 01:49:40 +0900 Subject: [PATCH 08/24] =?UTF-8?q?feat:=20=EC=A2=8C=EC=B8=A1=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 25854af..b6bd940 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -159,5 +159,81 @@ struct TodoEditorView: View { private enum Field: Hashable { case title, description, tag + +private 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 } } From 904fe4362c221907318b10433a1f3394f22ae9df Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 01:50:07 +0900 Subject: [PATCH 09/24] =?UTF-8?q?feat:=20=ED=83=9C=EA=B7=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index b6bd940..fc7e220 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -237,3 +237,59 @@ private struct TagLayout: Layout { var maxHeight: CGFloat = 0 } } + +private 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)) + } + } +} From 0533939106d5938786e663d875e2ab51e06e284a Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 09:18:21 +0900 Subject: [PATCH 10/24] =?UTF-8?q?refactor:=20=EB=8F=99=EC=9D=BC=ED=95=9C?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=EB=AA=85=EC=9D=BC=20=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Presentation/ViewModel/TodoEditorViewModel.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index 60fd638..d64d549 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -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 = [] var tagText: String = "" var focusOnEditor: Bool = false var hasDueDate: Bool { return dueDate != nil } @@ -29,6 +30,7 @@ final class TodoEditorViewModel: Store { enum Action { case addTag + case clearTagText case removeTag(String) case setContent(String) case setDueDate(Date) @@ -62,7 +64,7 @@ final class TodoEditorViewModel: Store { state.title = todo.title state.content = todo.content state.dueDate = todo.dueDate - state.tags = todo.tags + state.tags = OrderedSet(todo.tags) } } @@ -70,10 +72,12 @@ final class TodoEditorViewModel: Store { switch action { case .addTag: let tagText = state.tagText - if !state.tags.contains(tagText) && !tagText.isEmpty { + if !tagText.isEmpty { state.tags.append(tagText) state.tagText = "" } + case .clearTagText: + state.tagText = "" case .removeTag(let tagText): state.tags.removeAll { $0 == tagText } case .setContent(let stringValue), @@ -125,7 +129,7 @@ extension TodoEditorViewModel { createdAt: self.createdAt ?? date, updatedAt: date, dueDate: state.dueDate, - tags: state.tags, + tags: state.tags.map { $0 }, kind: self.kind ) } From bafac89fddde1ba0f46b32697e2501d5b6cf2a2a Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 11:28:38 +0900 Subject: [PATCH 11/24] =?UTF-8?q?feat:=20=EB=A7=88=EA=B0=90=EC=9D=BC=20?= =?UTF-8?q?=ED=94=BC=EC=BB=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Extension/EnvironmentValues+.swift | 31 +++++++++++++++ DevLog/UI/Home/TodoEditorView.swift | 41 ++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 DevLog/UI/Extension/EnvironmentValues+.swift diff --git a/DevLog/UI/Extension/EnvironmentValues+.swift b/DevLog/UI/Extension/EnvironmentValues+.swift new file mode 100644 index 0000000..a7fe565 --- /dev/null +++ b/DevLog/UI/Extension/EnvironmentValues+.swift @@ -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) + } +} diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index fc7e220..adf8a6d 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -293,3 +293,44 @@ private struct Tag: View { } } } +private struct DueDatePicker: View { + @Environment(\.safeAreaInsets) private var safeAreaInsets + @State private var isPresented: Bool = false + @State private var height: CGFloat = .pi + @Binding var dueDate: Date + @ViewBuilder private var content: () -> Content + + init( + selection dueDate: Binding, + @ViewBuilder content: @escaping () -> Content + ) { + self._dueDate = dueDate + self.content = content + } + + var body: some View { + Button { + isPresented.toggle() + } label: { + content() + } + .sheet(isPresented: $isPresented) { + DatePicker( + "", + selection: $dueDate, + displayedComponents: .date + ) + .labelsHidden() + .datePickerStyle(.graphical) + .presentationDragIndicator(.hidden) + .presentationDetents([.height(height)]) + .background { + GeometryReader { geometry in + Color.clear.onAppear { + height = geometry.size.height + safeAreaInsets.bottom + 16 + } + } + } + } + } +} From 1d4791fcb080d2507cc6858a95a9e1b94178b77a Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 6 Feb 2026 11:48:01 +0900 Subject: [PATCH 12/24] =?UTF-8?q?chore:=20Tag=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Common/Componeent/Tag+.swift | 142 +++++++++++++++++++++++++ DevLog/UI/Home/TodoEditorView.swift | 120 --------------------- 2 files changed, 142 insertions(+), 120 deletions(-) create mode 100644 DevLog/UI/Common/Componeent/Tag+.swift diff --git a/DevLog/UI/Common/Componeent/Tag+.swift b/DevLog/UI/Common/Componeent/Tag+.swift new file mode 100644 index 0000000..c0806af --- /dev/null +++ b/DevLog/UI/Common/Componeent/Tag+.swift @@ -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 + } +} diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index adf8a6d..1afe878 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -159,138 +159,18 @@ struct TodoEditorView: View { private enum Field: Hashable { case title, description, tag - -private 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 } } -private 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)) - } } } private struct DueDatePicker: View { From 058adc9068e2425182fe463fac53c89e9153ead8 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sat, 7 Feb 2026 21:35:21 +0900 Subject: [PATCH 13/24] =?UTF-8?q?refactor:=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EB=B0=8F=20=ED=99=95=EC=9D=B8=EC=9D=84=20?= =?UTF-8?q?sheet=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/TodoEditorViewModel.swift | 13 +- DevLog/Resource/Localizable.xcstrings | 3 + DevLog/UI/Extension/View+.swift | 8 +- DevLog/UI/Home/TodoEditorView.swift | 196 +++++++++++++++--- 4 files changed, 179 insertions(+), 41 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index d64d549..0df83db 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -29,8 +29,7 @@ final class TodoEditorViewModel: Store { } enum Action { - case addTag - case clearTagText + case addTag(String) case removeTag(String) case setContent(String) case setDueDate(Date) @@ -70,14 +69,8 @@ final class TodoEditorViewModel: Store { func reduce(with action: Action) -> [SideEffect] { switch action { - case .addTag: - let tagText = state.tagText - if !tagText.isEmpty { - state.tags.append(tagText) - state.tagText = "" - } - case .clearTagText: - 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), diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index 8d198d2..ef94b08 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -352,6 +352,9 @@ }, "태그" : { + }, + "태그 입력" : { + }, "테마" : { diff --git a/DevLog/UI/Extension/View+.swift b/DevLog/UI/Extension/View+.swift index 43cd522..de3f12c 100644 --- a/DevLog/UI/Extension/View+.swift +++ b/DevLog/UI/Extension/View+.swift @@ -31,8 +31,8 @@ extension View { } @ViewBuilder - func adaptiveButtonStyle() -> some View { - if #available(iOS 26.0, *) { + func adaptiveButtonStyle(_ color: Color? = nil) -> some View { + if #available(iOS 26.0, *), color == nil { self.buttonStyle(.glass) } else { self.foregroundStyle(Color(.label)) @@ -42,6 +42,10 @@ extension View { .background { Capsule() .fill(.ultraThinMaterial) + .background { + Capsule() + .fill(color ?? Color.clear) + } .overlay { Capsule() .stroke(Color.white.opacity(0.2), lineWidth: 1) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 1afe878..24311a9 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -5,8 +5,9 @@ // Created by opfic on 5/31/25. // -import SwiftUI import MarkdownUI +import OrderedCollections +import SwiftUI struct TodoEditorView: View { @StateObject var viewModel: TodoEditorViewModel @@ -37,33 +38,9 @@ struct TodoEditorView: View { .onTapGesture { field = .description } - HStack { - Button { - field = nil - } label: { - Label { - Text("태그") - } icon: { - Image(systemName: "tag") - .foregroundStyle(.gray) - } - } - .adaptiveButtonStyle() - - Button { - field = nil - showDueDatePicker = true - } label: { - Label { - Text("마감일") - } icon: { - Image(systemName: "calendar") - .foregroundStyle(.gray) - } - } - .adaptiveButtonStyle() - } - .padding(.bottom, 16 + safeAreaInsets.bottom / 4) + accessoryBar + .padding(.horizontal) + .padding(.bottom, 16 + safeAreaInsets.bottom / 4) } .ignoresSafeArea(.container, edges: .bottom) .navigationTitle(viewModel.navigationTitle) @@ -137,6 +114,36 @@ struct TodoEditorView: View { .padding(.top, 10) } + private var accessoryBar: some View { + HStack { + TagEditor( + tags: viewModel.state.tags, + addAction: { viewModel.send(.addTag($0)) }, + deleteAction: { viewModel.send(.removeTag($0)) } + ) { + Label { + Text("태그") + } icon: { + Image(systemName: "tag") + .foregroundStyle(.gray) + } + } + .adaptiveButtonStyle() + DueDatePicker(selection: Binding( + get: { viewModel.state.dueDate ?? Date() }, + set: { viewModel.send(.setDueDate($0)) } + )) { + Label { + Text("마감일") + } icon: { + Image(systemName: "calendar") + .foregroundStyle(.gray) + } + } + .adaptiveButtonStyle() + } + } + @ToolbarContentBuilder private var toolBar: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { @@ -162,17 +169,142 @@ struct TodoEditorView: View { } } +private struct TagEditor: View { + @Environment(\.safeAreaInsets) private var safeAreaInsets + @State private var isPresented: Bool = false + @State private var sheetHeight: CGFloat = .pi + @State private var tagsHeight: CGFloat = 0 + @State private var fieldHeight: CGFloat = 0 + @State private var tag = "" + @ViewBuilder private var content: () -> Content + @FocusState private var focused: Bool + private let tags: OrderedSet + private let addAction: (String) -> Void + private let deleteAction: (String) -> Void + private let spacing: CGFloat = 8 - self.action = action + init( + tags: OrderedSet, + addAction: @escaping (String) -> Void = { _ in }, + deleteAction: @escaping (String) -> Void = { _ in }, + @ViewBuilder content: @escaping () -> Content + ) { + self.tags = tags + self.addAction = addAction + self.deleteAction = deleteAction + self.content = content } var body: some View { + Button { + isPresented = true + } label: { + content() + } + .sheet(isPresented: $isPresented) { + VStack(spacing: tags.isEmpty ? 0 : 8) { + ScrollView { + TagLayout { + ForEach(tags, id: \.self) { tagText in + Tag(tagText, isEditing: true) { + deleteAction(tagText) + } + } + } + .background { + GeometryReader { geometry in + Color.clear + .onAppear { + // 처음부터 태그가 있을 때 호출될 듯? + // tagField의 onAppear 연산 후에 실행되도록 딜레이를 줌 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + print("ScrollView onAppear") + tagsHeight = geometry.size.height + sheetHeight += tagsHeight + (tagsHeight == 0 ? 0 : 8) + } + } + .onChange(of: tags) { newTags in + DispatchQueue.main.async { + tagsHeight = geometry.size.height + sheetHeight = fieldHeight + tagsHeight + (newTags.isEmpty ? 0 : 8) + print(tagsHeight, fieldHeight, sheetHeight, newTags.isEmpty) + } + } + } } } + .scrollIndicators(.hidden) + .frame(maxHeight: tagsHeight) + .padding(.top, tags.isEmpty || !focused ? 0 : 8) // 키보드 포커싱 중에는 패딩 끌 것 + + // 항상 나타나있음 + tagField + .background { + GeometryReader { geometry in + Color.clear + .onAppear { + fieldHeight = geometry.size.height + 16 + sheetHeight = fieldHeight + } + } + } + } + .padding(.horizontal) + .ignoresSafeArea(.all, edges: .bottom) + .presentationDragIndicator(.hidden) + .presentationDetents([.height(sheetHeight)]) + } + } + + private var tagField: some View { + HStack { + HStack { + TextField("태그 입력", text: $tag) + .keyboardType(.webSearch) + .focused($focused) + .onSubmit { + isPresented = false + } + .padding(tag.isEmpty ? .all : [.leading, .vertical]) + + if !tag.isEmpty { + Button { + tag = "" + } label: { + Image(systemName: "xmark.circle.fill") + .font(.title) + .symbolRenderingMode(.palette) + .foregroundStyle( + Color(.label), + Color(.systemBackground) + ) + } + .padding(.trailing) + } + } + .background { + Capsule() + .fill(.ultraThinMaterial) + .overlay { + Capsule() + .stroke(Color.white.opacity(0.2), lineWidth: 1) + } + } + + Button { + addAction(tag) + tag = "" + } label: { + Image(systemName: "plus") + .font(.title.bold()) + .padding(.vertical, 5) + } + .adaptiveButtonStyle((!tag.isEmpty && !tags.contains(tag)) ? .blue : .clear) } } } + private struct DueDatePicker: View { @Environment(\.safeAreaInsets) private var safeAreaInsets @State private var isPresented: Bool = false @@ -214,3 +346,9 @@ private struct DueDatePicker: View { } } } + +#Preview { + TodoEditorView( + viewModel: TodoEditorViewModel(title: "123") + ) +} From b1bb314edeb881eb18b8ff3d41d879ae8c5c50d7 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sat, 7 Feb 2026 23:57:16 +0900 Subject: [PATCH 14/24] =?UTF-8?q?style:=20Preview=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 24311a9..d0b7ec5 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -346,9 +346,3 @@ private struct DueDatePicker: View { } } } - -#Preview { - TodoEditorView( - viewModel: TodoEditorViewModel(title: "123") - ) -} From d1cb6e41a1bf8107fec36c86a82bf01afc2de31a Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:12:14 +0900 Subject: [PATCH 15/24] =?UTF-8?q?refactor:=20focus=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index d0b7ec5..c3bcfe3 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -177,7 +177,6 @@ private struct TagEditor: View { @State private var fieldHeight: CGFloat = 0 @State private var tag = "" @ViewBuilder private var content: () -> Content - @FocusState private var focused: Bool private let tags: OrderedSet private let addAction: (String) -> Void private let deleteAction: (String) -> Void @@ -235,7 +234,7 @@ private struct TagEditor: View { } .scrollIndicators(.hidden) .frame(maxHeight: tagsHeight) - .padding(.top, tags.isEmpty || !focused ? 0 : 8) // 키보드 포커싱 중에는 패딩 끌 것 + .padding(.top, tags.isEmpty ? 0 : 8) // 항상 나타나있음 tagField @@ -262,7 +261,6 @@ private struct TagEditor: View { HStack { TextField("태그 입력", text: $tag) .keyboardType(.webSearch) - .focused($focused) .onSubmit { isPresented = false } From 5f354839f36039a34e1f60ff324a29624adba77e Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:15:19 +0900 Subject: [PATCH 16/24] =?UTF-8?q?feat:=20=EC=8B=9C=ED=8A=B8=EA=B0=80=20?= =?UTF-8?q?=EB=82=B4=EB=A0=A4=EA=B0=88=20=EB=95=8C=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=ED=96=88=EB=8D=98=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index c3bcfe3..1c3fdf0 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -200,8 +200,10 @@ private struct TagEditor: View { } label: { content() } - .sheet(isPresented: $isPresented) { - VStack(spacing: tags.isEmpty ? 0 : 8) { + .sheet( + isPresented: $isPresented, + onDismiss: { tag = "" } + ) { ScrollView { TagLayout { ForEach(tags, id: \.self) { tagText in From 6bcfacc53ad1acad88599e9cc51793678ec54c29 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:15:47 +0900 Subject: [PATCH 17/24] =?UTF-8?q?feat:=20spacing=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=BC=EA=B4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 1c3fdf0..861f0d2 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -204,6 +204,7 @@ private struct TagEditor: View { isPresented: $isPresented, onDismiss: { tag = "" } ) { + VStack(spacing: tags.isEmpty ? 0 : spacing) { ScrollView { TagLayout { ForEach(tags, id: \.self) { tagText in @@ -221,14 +222,14 @@ private struct TagEditor: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { print("ScrollView onAppear") tagsHeight = geometry.size.height - sheetHeight += tagsHeight + (tagsHeight == 0 ? 0 : 8) + sheetHeight += tagsHeight + (tagsHeight == 0 ? 0 : spacing) } } .onChange(of: tags) { newTags in DispatchQueue.main.async { tagsHeight = geometry.size.height - sheetHeight = fieldHeight + tagsHeight + (newTags.isEmpty ? 0 : 8) print(tagsHeight, fieldHeight, sheetHeight, newTags.isEmpty) + sheetHeight = fieldHeight + tagsHeight + (newTags.isEmpty ? 0 : spacing) } } } From 78df44f3eb069792f4c5e4c66ea0307c6622b18e Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:16:26 +0900 Subject: [PATCH 18/24] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 861f0d2..0bb583d 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -217,10 +217,7 @@ private struct TagEditor: View { GeometryReader { geometry in Color.clear .onAppear { - // 처음부터 태그가 있을 때 호출될 듯? - // tagField의 onAppear 연산 후에 실행되도록 딜레이를 줌 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - print("ScrollView onAppear") tagsHeight = geometry.size.height sheetHeight += tagsHeight + (tagsHeight == 0 ? 0 : spacing) } @@ -228,7 +225,6 @@ private struct TagEditor: View { .onChange(of: tags) { newTags in DispatchQueue.main.async { tagsHeight = geometry.size.height - print(tagsHeight, fieldHeight, sheetHeight, newTags.isEmpty) sheetHeight = fieldHeight + tagsHeight + (newTags.isEmpty ? 0 : spacing) } } @@ -239,7 +235,6 @@ private struct TagEditor: View { .frame(maxHeight: tagsHeight) .padding(.top, tags.isEmpty ? 0 : 8) - // 항상 나타나있음 tagField .background { GeometryReader { geometry in @@ -264,10 +259,10 @@ private struct TagEditor: View { HStack { TextField("태그 입력", text: $tag) .keyboardType(.webSearch) + .padding(tag.isEmpty ? .all : [.leading, .vertical]) .onSubmit { isPresented = false } - .padding(tag.isEmpty ? .all : [.leading, .vertical]) if !tag.isEmpty { Button { From 7f16c1e20e7dd2733a7b0231d97c631da1949e13 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:17:08 +0900 Subject: [PATCH 19/24] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=ED=95=A0=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=B4=20?= =?UTF-8?q?=EC=97=86=EA=B1=B0=EB=82=98=20=ED=8F=AC=ED=95=A8=EB=90=98?= =?UTF-8?q?=EC=96=B4=20=EC=9E=88=EB=8B=A4=EB=A9=B4=20=ED=83=AD=20=EC=9D=B4?= =?UTF-8?q?=ED=8E=99=ED=8A=B8=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 0bb583d..cc63aa7 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -297,6 +297,7 @@ private struct TagEditor: View { .padding(.vertical, 5) } .adaptiveButtonStyle((!tag.isEmpty && !tags.contains(tag)) ? .blue : .clear) + .disabled(tag.isEmpty || tags.contains(tag)) } } } From 20fcc64b055075ce93e7bdcd1cfc025a78fc50e0 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 01:52:58 +0900 Subject: [PATCH 20/24] =?UTF-8?q?feat:=20=EB=A7=88=EA=B0=90=EC=9D=BC=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/TodoEditorViewModel.swift | 6 ++-- DevLog/UI/Home/TodoEditorView.swift | 30 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index 0df83db..4b8810d 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -32,7 +32,7 @@ final class TodoEditorViewModel: Store { case addTag(String) case removeTag(String) case setContent(String) - case setDueDate(Date) + case setDueDate(Date?) case setTabViewTag(Tag) case setTagText(String) case setTitle(String) @@ -78,8 +78,10 @@ final class TodoEditorViewModel: Store { .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 diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index cc63aa7..39606d6 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -133,11 +133,22 @@ struct TodoEditorView: View { get: { viewModel.state.dueDate ?? Date() }, set: { viewModel.send(.setDueDate($0)) } )) { - Label { - Text("마감일") - } icon: { - Image(systemName: "calendar") - .foregroundStyle(.gray) + HStack { + Label { + Text("마감일") + } icon: { + Image(systemName: "calendar") + .foregroundStyle(.gray) + } + Image(systemName: "checkmark.square") + .symbolRenderingMode(.palette) + .foregroundStyle( + viewModel.state.hasDueDate ? .blue : .clear, + .gray + ) + .onTapGesture { + viewModel.send(.setDueDate(viewModel.state.hasDueDate ? nil : Date())) + } } } .adaptiveButtonStyle() @@ -147,11 +158,12 @@ struct TodoEditorView: View { @ToolbarContentBuilder private var toolBar: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { - Button(action: { + Button { dismiss() - }) { - Image(systemName: "xmark")} - .bold() + } label: { + Image(systemName: "xmark") + .bold() + } } ToolbarItem(placement: .topBarTrailing) { Button(action: { From 39aaedfc838bc2c91332d554f8301c1a473b4d69 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 02:01:40 +0900 Subject: [PATCH 21/24] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20ignoresSafeArea=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 39606d6..be6c9b4 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -42,7 +42,6 @@ struct TodoEditorView: View { .padding(.horizontal) .padding(.bottom, 16 + safeAreaInsets.bottom / 4) } - .ignoresSafeArea(.container, edges: .bottom) .navigationTitle(viewModel.navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { toolBar } @@ -260,7 +259,6 @@ private struct TagEditor: View { } .padding(.horizontal) - .ignoresSafeArea(.all, edges: .bottom) .presentationDragIndicator(.hidden) .presentationDetents([.height(sheetHeight)]) } From b3aafa69e9aff0ceabd3957203190358dcc3ed7c Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 02:24:37 +0900 Subject: [PATCH 22/24] =?UTF-8?q?fix:=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EC=8B=9C=20=ED=88=B4=EB=B0=94=20=EB=B0=B0=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EA=B3=BC=20header=EA=B3=BC=20=EC=96=B4?= =?UTF-8?q?=EC=9A=B0=EB=9F=AC=EC=A7=80=EC=A7=80=20=EC=95=8A=EC=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index be6c9b4..39bdd26 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -44,6 +44,7 @@ struct TodoEditorView: View { } .navigationTitle(viewModel.navigationTitle) .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.background, for: .navigationBar) .toolbar { toolBar } } } From 92f1cdbc3dbc834f9854d0933f258ba93035418e Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 02:37:34 +0900 Subject: [PATCH 23/24] =?UTF-8?q?fix:=20Todo=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20kind=EA=B0=80=20etc=EB=A1=9C=EB=A7=8C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/TodoEditorViewModel.swift | 38 ++++++++++++------- DevLog/UI/Home/TodoDetailView.swift | 2 +- DevLog/UI/Home/TodoView.swift | 3 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift index 4b8810d..c59eb77 100644 --- a/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift +++ b/DevLog/Presentation/ViewModel/TodoEditorViewModel.swift @@ -51,20 +51,30 @@ 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 = OrderedSet(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] { diff --git a/DevLog/UI/Home/TodoDetailView.swift b/DevLog/UI/Home/TodoDetailView.swift index 7d726b3..b02cc4c 100644 --- a/DevLog/UI/Home/TodoDetailView.swift +++ b/DevLog/UI/Home/TodoDetailView.swift @@ -62,7 +62,7 @@ struct TodoDetailView: View { } .fullScreenCover(isPresented: $showEditor) { TodoEditorView( - viewModel: TodoEditorViewModel(title: "수정", todo: todo), + viewModel: TodoEditorViewModel(todo: todo), onSubmit: { onSubmit?($0) } ) } diff --git a/DevLog/UI/Home/TodoView.swift b/DevLog/UI/Home/TodoView.swift index 49350da..cacddaa 100644 --- a/DevLog/UI/Home/TodoView.swift +++ b/DevLog/UI/Home/TodoView.swift @@ -98,9 +98,8 @@ struct TodoView: View { get: { viewModel.state.showEditor }, set: { _, _ in viewModel.send(.closeEditor) }) ) { - let title = viewModel.state.kind.localizedName TodoEditorView( - viewModel: TodoEditorViewModel(title: title), + viewModel: TodoEditorViewModel(kind: viewModel.state.kind), onSubmit: { viewModel.send(.upsertTodo($0)) } ) } From fe0d240275dde2a72f6f6b33febb6b53626f8ff2 Mon Sep 17 00:00:00 2001 From: opficdev Date: Sun, 8 Feb 2026 02:45:42 +0900 Subject: [PATCH 24/24] =?UTF-8?q?fix:=20DatePicker=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=EB=B3=B4=EB=8B=A4=20sheet=EC=9D=98=20safeArea=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20Picker=EC=9D=B4=20=EA=B0=80=EB=A0=A4?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=ED=98=84=EC=83=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Home/TodoEditorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevLog/UI/Home/TodoEditorView.swift b/DevLog/UI/Home/TodoEditorView.swift index 39bdd26..c0a6f48 100644 --- a/DevLog/UI/Home/TodoEditorView.swift +++ b/DevLog/UI/Home/TodoEditorView.swift @@ -347,7 +347,7 @@ private struct DueDatePicker: View { .background { GeometryReader { geometry in Color.clear.onAppear { - height = geometry.size.height + safeAreaInsets.bottom + 16 + height = geometry.size.height + safeAreaInsets.bottom + safeAreaInsets.top } } }