Skip to content
Merged
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
35 changes: 24 additions & 11 deletions Mosco/App/Common/DesignSystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,13 @@ Toss/Apple류의 절제된 UI를 기준으로 삼는다 — 원색 대신 톤
걷어냈다 — 탭 바에 할 일 탭이 늘 보이므로 **그 줄이 가리키는 곳이 한 번
누르면 닿는 자리**고, 매일 보는 화면에서 "저기 더 있다"를 되풀이할 이유가 없다.
남은 개수는 '할 일 N개 남음' 머리글이 말한다(오늘일 때만).
- **디데이는 카드가 아니라 행에 붙는 `D-7` 조각이다 (1.4.4).** 카드가 답하던 것이
"며칠 남았나" 하나뿐이었고, 그건 섹션 하나를 차지할 질문이 아니다. 날짜가 함께
보이는 화면(할 일 탭)에서만 숫자로 세고, 하루 페이지에서는 날짜가 이미 머리에
있으니 깃발만 둔다. 지난 디데이는 `D+3`으로 계속 보인다 — 지났다고 숨기면 왜
사라졌는지 알 수 없고, 지난 디데이야말로 눈에 띄어야 한다.
- **디데이 카드는 할 일 탭 맨 위로 옮겼다 (1.4.4).** 오늘 페이지에서 걷어냈다가
할 일 탭 머리에 되살렸다. 그 사이에 **목록 안에서 디데이를 말해보려고 두 번
시도했고 둘 다 되돌렸다** — 행에 `D-7` 조각을 붙이면 태그 줄만 길어지고,
'나중' 칸에서 디데이를 맨 위로 끌어올리면 날짜순이 깨져 목록을 읽는 리듬이
끊긴다. 남은 날을 세는 일은 **목록 바깥**이 맡는 게 맞다. 가로로 넘겨 보는
카드이고 가장 가까운 하나만 채운다. 행에는 깃발만 둔다. 지난 디데이도 `D+3`으로
계속 보인다 — 지났다고 숨기면 왜 사라졌는지 알 수 없다.
- **메모는 오늘 전문, 다른 날은 세 줄.** 예전 오늘 탭과 하루 페이지의 규칙을
그대로 옮겼다.
- **돌아가기 버튼은 필요할 때만 떠오른다.** 다른 달을 보면 '↶ 이번 달', 다른 날
Expand All @@ -330,12 +332,23 @@ Toss/Apple류의 절제된 UI를 기준으로 삼는다 — 원색 대신 톤
밀려 들어오면 쓰던 것을 잃은 것처럼 보인다. 하루 페이지를 열 때는 키보드부터 내린다.
- **검색은 달력 머리의 돋보기.** 오늘 탭 머리에 있던 것을 옮겼다. 제목·메모, 날짜와
무관하게 전체. 결과의 수정 버튼은 그날 페이지로 데려간다.
- **할 일 탭은 날짜가 아니라 급한 순서로 묶는다 (1.4.4).** 지난 것 → 오늘 →
이번 주 → 나중 → 날짜 없음. 달력이 이미 "언제"에 답하므로, 이 탭은 "무엇부터"에
답한다 — 위에서부터 읽으면 그게 곧 할 순서다. '이번 주'는 달력의 주가 아니라
**앞으로 이레**다(수요일에 열었는데 이틀치만 보이면 쓸모가 없다). '나중'에서만
디데이를 맨 위로 올린다 — 석 달 뒤 시험은 날짜순으로는 저 아래인데, 디데이로
표시했다는 건 자주 보고 싶다는 뜻이다. 판정은 `TodoListPage`(Shared).
- **할 일 탭은 손이 가야 하는 것 먼저, 그다음 시간순이다 (1.4.4).**
디데이 카드 → 지난 할 일 → 날짜 없음 → 오늘 → 이번 주 → 나중.
앞의 둘은 **"언제 할지 아직 안 정해진 것"**이라 사람이 손을 대야 움직인다.
날짜가 이미 정해진 것들은 그 뒤에 시간순으로 따라온다. 달력이 이미 "언제"에
답하므로 이 탭은 "무엇부터"에 답한다. '이번 주'는 달력의 주가 아니라 **앞으로
이레**다(수요일에 열었는데 이틀치만 보이면 쓸모가 없다). 판정은
`TodoListPage`(Shared).

묶음 이름은 **짧게** 쓴다 — '날짜를 안 정한 할 일'이 아니라 '날짜 없음'이다.
머리글은 훑을 때 위치를 알려주는 표지라 한눈에 들어오는 길이가 맞고, 같은 뜻을
입력창의 날짜 칩(`날짜 없음`)에서도 쓰고 있어 둘이 같은 말을 같은 말로 한다.
- **'오늘로 가져오기'는 아이콘이 아니라 글자다 (1.4.4).** '지난 할 일'과
'날짜 없음' 칸의 행 오른쪽에 `오늘로` 칩이 선다. 예전엔 위를 가리키는 화살표
동그라미였는데, 그 화살표가 무엇을 뜻하는지 알 수가 없었다 — 위로 올린다는
건지, 순서를 바꾼다는 건지, 접는다는 건지. **날짜를 오늘로 바꾸는 일에는
가리킬 방향이 애초에 없다.** 아이콘이 뜻을 못 나르는 자리에서는 글자가 낫다.
셀의 다른 칩과 같은 모양이라 줄에 섞여도 튀지 않는다.
- **할 일 탭에도 같은 입력창이 있고, 거기서 적으면 날짜 없이 저장된다.**
1.4.0에서 탭을 없앴던 이유가 "입력창을 못 찾아서"였으니 새 탭을 만들며 같은
실수를 되풀이하지 않는다. 날짜를 가로지르는 목록이라 오늘로 밀어넣지도 않는다 —
Expand Down
29 changes: 5 additions & 24 deletions Mosco/App/Features/Calendar/TodoRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,6 @@ struct TodoRow: View {
/// 입구라 각자 자기 상태를 갖는다.
@State private var showsMemoEditor = false

/// `D-7` · `D-DAY` · `D+3`. 지난 디데이도 센다 — 지났다고 숨기면 왜 사라졌는지
/// 알 수 없고, 지난 디데이야말로 눈에 띄어야 하는 것이다.
private func dDayLabel(_ days: Int) -> String {
switch days {
case 0: String(localized: "D-DAY")
case 1...: String(localized: "D-\(days)")
default: String(localized: "D+\(-days)")
}
}

private var isDone: Bool {
if let occurrenceDate { return todo.isCompleted(on: occurrenceDate) }
return todo.isCompleted
Expand Down Expand Up @@ -119,25 +109,16 @@ struct TodoRow: View {
// 디데이로 표시해둔 항목. 남은 날짜는 적지 않는다 — 옆의 날짜
// 태그와 같은 말을 두 번 하는 셈이고, 여기서 알고 싶은 건
// "이게 그 챙기는 일이구나"뿐이다. 남은 날짜를 세는 일은
// 날짜가 함께 보이는 화면(할 일 탭)에서만 한다.
// 할 일 탭 맨 위의 디데이 카드가 맡는다 — 한 줄에 두 번
// 말하면 태그 줄만 길어진다.
//
// 별이 아니라 깃발인 건, 별은 어느 앱에서나 "즐겨찾기/중요"라는
// 뜻으로 굳어 있어서다. 디데이는 중요도가 아니라 **날을 세는
// 표시**고, 깃발은 달력 위 한 지점을 찍는 말에 가깝다.
if todo.isDDay {
// **날짜가 보이는 화면에서는 남은 날을 센다.** 할 일 탭은
// 디데이 카드를 따로 두지 않고 이 한 조각으로 대신한다 —
// 카드가 답하던 것이 "며칠 남았나" 하나뿐이었다.
// 하루 페이지에서는 날짜가 이미 머리에 있어서 깃발만 둔다.
if showsDate, let days = todo.dDayCount(from: .now) {
Text(dDayLabel(days))
.font(.system(size: 10, weight: .bold))
.foregroundStyle(MoscoPalette.textSecondary)
} else {
Image(systemName: "flag.fill")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(MoscoPalette.textSecondary)
}
Image(systemName: "flag.fill")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(MoscoPalette.textSecondary)
}

// 반복 일정은 한 번짜리와 겉모습이 같아서, 목록에서 이게
Expand Down
130 changes: 109 additions & 21 deletions Mosco/App/Features/TodoList/TodoListScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SwiftUI
/// ## 왜 셋째 탭인가
///
/// 달력은 "언제"에 답한다. 거기에만 기대면 **날짜를 아직 안 정한 일이 설 자리가
/// 없다.** 지금까지는 오늘 페이지 맨 아래 '날짜를 안 정한 할 일' 칸에 들어 있었는데 —
/// 없다.** 지금까지는 오늘 페이지 맨 아래 '날짜 없음' 칸에 들어 있었는데 —
/// 적어는 뒀는데 오늘을 열어야만 보이고, 그것도 오늘 할 일들 **아래**에 있었다.
/// 할 일 앱으로 쓰려는 사람에게는 그게 본진인데 부속으로 놓여 있던 셈이다.
///
Expand Down Expand Up @@ -76,12 +76,17 @@ private struct TodoListContent: View {
}
}

private var dDayTodos: [TodoItem] {
TodoListPage.dDays(in: visibleTodos, today: today)
}

@ViewBuilder
private var list: some View {
if sections.isEmpty {
if sections.isEmpty && dDayTodos.isEmpty {
emptyState
} else {
List {
if !dDayTodos.isEmpty { dDaySection }
ForEach(sections, id: \.section) { group in
Section {
ForEach(group.todos) { todo in
Expand All @@ -102,6 +107,69 @@ private struct TodoListContent: View {
}
}

/// 맨 위 디데이 줄. **세로로 쌓지 않고 가로로 넘겨 본다** — 세로로 쌓으면
/// 디데이 몇 개만으로 정작 할 일이 화면 밖으로 밀려난다.
private var dDaySection: some View {
Section {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(dDayTodos) { todo in
dDayCard(todo, isNearest: todo.id == dDayTodos.first?.id)
}
}
.padding(.horizontal, Metrics.spacingMD)
}
.scrollClipDisabled()
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 2, leading: 0, bottom: 0, trailing: 0))
} header: {
header(title: String(localized: "디데이"), count: nil, tinted: false)
}
}

/// 가장 가까운 하나만 채운다 — 나머지는 옅게. 그래야 "다음은 이것"이 한눈에 선다.
private func dDayCard(_ todo: TodoItem, isNearest: Bool) -> some View {
let day = Calendar.current.startOfDay(for: todo.date ?? today)
let left = Calendar.current.dateComponents([.day], from: today, to: day).day ?? 0
return VStack(alignment: .leading, spacing: 0) {
Text(dDayLabel(left))
.font(.system(size: 27, weight: .heavy, design: .rounded).monospacedDigit())
.foregroundStyle(isNearest ? .white : MoscoPalette.accent)
.lineLimit(1)
.minimumScaleFactor(0.7)
Spacer(minLength: 6)
Text(todo.title)
.font(.moscoCaption().weight(.semibold))
.foregroundStyle(isNearest ? .white : MoscoPalette.textPrimary)
.lineLimit(1)
Text(day.localizedMonthDayWeekday)
.font(.system(size: 11))
.foregroundStyle(isNearest ? .white.opacity(0.75) : MoscoPalette.textSecondary)
.lineLimit(1)
}
.padding(14)
.frame(width: 138, height: 112, alignment: .topLeading)
// 유리도 그림자도 쓰지 않는다 — 셀과 같은 이유로 단색 채우기만 쓴다.
.background(
isNearest ? MoscoPalette.accent : MoscoPalette.accent.opacity(0.12),
in: RoundedRectangle(cornerRadius: 20, style: .continuous)
)
.contentShape(Rectangle())
.onTapGesture { editingTodo = todo }
.accessibilityElement(children: .combine)
}

/// `D-7` · `D-DAY` · `D+3`. 지난 디데이도 센다 — 지났다고 숨기면 왜 사라졌는지
/// 알 수 없고, 지난 디데이야말로 눈에 띄어야 하는 것이다.
private func dDayLabel(_ days: Int) -> String {
switch days {
case 0: String(localized: "D-DAY")
case 1...: String(localized: "D-\(days)")
default: String(localized: "D+\(-days)")
}
}

private func row(_ todo: TodoItem, in section: TodoListPage.Section) -> some View {
HStack(spacing: 8) {
TodoRow(
Expand All @@ -118,20 +186,10 @@ private struct TodoListContent: View {
memoDisplay: .compact
)

// 지난 것과 날짜 없는 것에만 '오늘로 가져오기'를 붙인다. 앞날 일정은
// 당길 이유가 없고, 오늘 것은 이미 오늘이다.
// 지난 것과 날짜 없는 것에만 붙인다. 앞날 일정은 당길 이유가 없고,
// 오늘 것은 이미 오늘이다.
if section == .overdue || section == .noDate {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
todo.pullIntoToday(today)
}
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 24))
.foregroundStyle(MoscoPalette.accent)
}
.buttonStyle(.plain)
.accessibilityLabel("오늘로 가져오기")
pullIntoTodayButton(todo)
}
}
.listRowBackground(Color.clear)
Expand All @@ -146,21 +204,51 @@ private struct TodoListContent: View {
)
}

/// **글자로 쓴다.** 예전엔 위를 가리키는 화살표 동그라미였는데, 그 화살표가
/// 무엇을 뜻하는지 알 수가 없었다 — 위로 올린다는 건지, 순서를 바꾼다는 건지,
/// 접는다는 건지. 날짜를 오늘로 바꾸는 일에는 가리킬 방향이 애초에 없다.
/// 아이콘이 뜻을 못 나르면 글자가 낫다. 셀의 다른 칩과 같은 모양이라 줄에
/// 섞여도 튀지 않는다.
private func pullIntoTodayButton(_ todo: TodoItem) -> some View {
Button {
withAnimation(.easeInOut(duration: 0.25)) {
todo.pullIntoToday(today)
}
} label: {
Text("오늘로")
.font(.moscoCaption().weight(.semibold))
.foregroundStyle(MoscoPalette.accent)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(MoscoPalette.accent.opacity(0.12), in: Capsule())
.contentShape(Capsule())
}
.buttonStyle(.plain)
.accessibilityLabel("오늘로 가져오기")
.accessibilityHint("날짜를 오늘로 바꿉니다")
}

/// 화면에 두 개 이상의 캘린더가 섞여 있을 때만 소속을 밝힌다.
private var showsCalendarTag: Bool {
Set(visibleTodos.compactMap { $0.calendar?.id }).count > 1
}

private func header(_ section: TodoListPage.Section, count: Int) -> some View {
header(title: title(for: section), count: count, tinted: section == .overdue)
}

/// `tinted`는 지난 할 일에만 쓴다. **한 화면에 경고색이 여러 번 나오면 아무것도
/// 경고가 아니게 된다**(DesignSystem/README.md의 "색은 한 행에 한 번").
private func header(title: String, count: Int?, tinted: Bool) -> some View {
HStack(spacing: 6) {
Text(title(for: section))
Text("\(count)").foregroundStyle(MoscoPalette.textSecondary.opacity(0.6))
Text(title)
if let count {
Text("\(count)").foregroundStyle(MoscoPalette.textSecondary.opacity(0.6))
}
Spacer()
}
.font(.moscoCaption())
// 지난 것만 색을 쓴다. **한 화면에 경고색이 여러 번 나오면 아무것도
// 경고가 아니게 된다**(DesignSystem/README.md의 "색은 한 행에 한 번").
.foregroundStyle(section == .overdue ? MoscoPalette.must : MoscoPalette.textSecondary)
.foregroundStyle(tinted ? MoscoPalette.must : MoscoPalette.textSecondary)
}

private func title(for section: TodoListPage.Section) -> String {
Expand All @@ -169,7 +257,7 @@ private struct TodoListContent: View {
case .today: String(localized: "오늘")
case .thisWeek: String(localized: "이번 주")
case .later: String(localized: "나중")
case .noDate: String(localized: "날짜를 안 정한 할 일")
case .noDate: String(localized: "날짜 없음")
}
}

Expand Down
Loading
Loading