diff --git a/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate index 9cb6840..daf4154 100644 Binary files a/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate and b/myApp/HaruDanim/HaruDanim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/myApp/HaruDanim/IOS/Views/ActionTabView.swift b/myApp/HaruDanim/IOS/Views/ActionTabView.swift new file mode 100644 index 0000000..d8a15f8 --- /dev/null +++ b/myApp/HaruDanim/IOS/Views/ActionTabView.swift @@ -0,0 +1,183 @@ +// +// ActionTabView.swift +// HaruDanim +// +// Lists all `Action`s and hosts a sheet for creating new ones. +// + +import SwiftUI +import SwiftData + +struct ActionTabView: View { + @Environment(\.modelContext) private var modelContext + @Query(sort: \Action.createdAt, order: .reverse) private var actions: [Action] + + @State private var isAddSheetPresented = false + + var body: some View { + NavigationStack { + List { + ForEach(actions) { action in + HStack(spacing: 12) { + Image(systemName: action.iconName) + .frame(width: 28) + .foregroundStyle(Color.brandPrimary) + + VStack(alignment: .leading, spacing: 4) { + Text(action.name) + if !action.tags.isEmpty { + Text(action.tags.map(\.name).joined(separator: ", ")) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Text(action.type == .time ? "시간" : "횟수") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .onDelete(perform: deleteActions) + } + .navigationTitle("행동") + .overlay { + if actions.isEmpty { + ContentUnavailableView( + "행동이 없어요", + systemImage: "bolt", + description: Text("오른쪽 위 + 버튼으로 행동을 추가하세요.") + ) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + isAddSheetPresented = true + } label: { + Label("행동 추가", systemImage: "plus") + } + } + } + .sheet(isPresented: $isAddSheetPresented) { + AddActionSheet() + } + } + } + + private func deleteActions(at offsets: IndexSet) { + for index in offsets { + modelContext.delete(actions[index]) + } + } +} + +// MARK: - Add Action Sheet + +private struct AddActionSheet: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @Query(sort: \Tag.createdAt, order: .reverse) private var allTags: [Tag] + + @State private var name = "" + @State private var iconName = "star.fill" + @State private var type: ActionType = .time + @State private var selectedTagIDs: Set = [] + + private var trimmedName: String { + name.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var body: some View { + NavigationStack { + Form { + Section("이름") { + TextField("행동 이름", text: $name) + } + + Section("아이콘 (SF Symbol)") { + HStack { + Image(systemName: iconName.isEmpty ? "questionmark" : iconName) + .foregroundStyle(Color.brandPrimary) + TextField("star.fill", text: $iconName) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + } + + Section("유형") { + Picker("유형", selection: $type) { + Text("시간").tag(ActionType.time) + Text("횟수").tag(ActionType.count) + } + .pickerStyle(.segmented) + } + + Section("꼬리표") { + if allTags.isEmpty { + Text("먼저 꼬리표를 추가하세요.") + .foregroundStyle(.secondary) + } else { + ForEach(allTags) { tag in + Button { + toggle(tag) + } label: { + HStack(spacing: 12) { + Circle() + .fill(Color(hex: tag.colorHex)) + .frame(width: 16, height: 16) + Text(tag.name) + .foregroundStyle(.primary) + Spacer() + if selectedTagIDs.contains(tag.id) { + Image(systemName: "checkmark") + .foregroundStyle(Color.brandPrimary) + } + } + } + } + } + } + } + .navigationTitle("새 행동") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("취소") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("저장") { save() } + .disabled(trimmedName.isEmpty) + } + } + } + } + + private func toggle(_ tag: Tag) { + if selectedTagIDs.contains(tag.id) { + selectedTagIDs.remove(tag.id) + } else { + selectedTagIDs.insert(tag.id) + } + } + + private func save() { + let chosenTags = allTags.filter { selectedTagIDs.contains($0.id) } + let trimmedIcon = iconName.trimmingCharacters(in: .whitespacesAndNewlines) + let action = Action( + name: trimmedName, + type: type, + iconName: trimmedIcon.isEmpty ? "circle" : trimmedIcon, + tags: chosenTags + ) + modelContext.insert(action) + dismiss() + } +} + +#Preview { + ActionTabView() + .modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true) +} diff --git a/myApp/HaruDanim/IOS/Views/MainTabView.swift b/myApp/HaruDanim/IOS/Views/MainTabView.swift index b9480c4..c67512d 100644 --- a/myApp/HaruDanim/IOS/Views/MainTabView.swift +++ b/myApp/HaruDanim/IOS/Views/MainTabView.swift @@ -15,12 +15,12 @@ struct MainTabView: View { Label("메인", systemImage: "house") } - Text("행동") + ActionTabView() .tabItem { Label("행동", systemImage: "bolt") } - Text("꼬리표") + TagTabView() .tabItem { Label("꼬리표", systemImage: "tag") } diff --git a/myApp/HaruDanim/IOS/Views/TagTabView.swift b/myApp/HaruDanim/IOS/Views/TagTabView.swift new file mode 100644 index 0000000..6c6458c --- /dev/null +++ b/myApp/HaruDanim/IOS/Views/TagTabView.swift @@ -0,0 +1,126 @@ +// +// TagTabView.swift +// HaruDanim +// +// Lists all `Tag`s and hosts a sheet for creating new ones. +// + +import SwiftUI +import SwiftData + +struct TagTabView: View { + @Environment(\.modelContext) private var modelContext + @Query(sort: \Tag.createdAt, order: .reverse) private var tags: [Tag] + + @State private var isAddSheetPresented = false + + var body: some View { + NavigationStack { + List { + ForEach(tags) { tag in + HStack(spacing: 12) { + Circle() + .fill(Color(hex: tag.colorHex)) + .frame(width: 20, height: 20) + Text(tag.name) + Spacer() + } + } + .onDelete(perform: deleteTags) + } + .navigationTitle("꼬리표") + .overlay { + if tags.isEmpty { + ContentUnavailableView( + "꼬리표가 없어요", + systemImage: "tag", + description: Text("오른쪽 위 + 버튼으로 꼬리표를 추가하세요.") + ) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + isAddSheetPresented = true + } label: { + Label("꼬리표 추가", systemImage: "plus") + } + } + } + .sheet(isPresented: $isAddSheetPresented) { + AddTagSheet() + } + } + } + + private func deleteTags(at offsets: IndexSet) { + for index in offsets { + modelContext.delete(tags[index]) + } + } +} + +// MARK: - Add Tag Sheet + +private struct AddTagSheet: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @State private var name = "" + @State private var color = Color(hex: "#2F6B4F") + + private var trimmedName: String { + name.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var body: some View { + NavigationStack { + Form { + Section("이름") { + TextField("꼬리표 이름", text: $name) + } + + Section("색상") { + ColorPicker("색상 선택", selection: $color, supportsOpacity: false) + } + } + .navigationTitle("새 꼬리표") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("취소") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("저장") { save() } + .disabled(trimmedName.isEmpty) + } + } + } + } + + private func save() { + let tag = Tag(name: trimmedName, colorHex: color.toHex()) + modelContext.insert(tag) + dismiss() + } +} + +// MARK: - Color → hex helper + +extension Color { + /// Serializes the color to a `#RRGGBB` string for persistence. + func toHex() -> String { + let resolved = resolve(in: EnvironmentValues()) + return String( + format: "#%02X%02X%02X", + Int((resolved.red * 255).rounded()), + Int((resolved.green * 255).rounded()), + Int((resolved.blue * 255).rounded()) + ) + } +} + +#Preview { + TagTabView() + .modelContainer(for: [Action.self, Tag.self, TimeSession.self, CountEntry.self], inMemory: true) +}