diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index e870fd3..9bccaf9 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -88,7 +88,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" | `CountEntry` | timestamp, amount, note | | | `Goal` | uuid, title, symbolName, colorHex, startDate, endDate?, statusRaw(inProgress/achieved/notAchieved), **achieveThresholdPercent(기본 100)**, sortOrder | isCollapsed·showsOnMain은 레거시 | | `Quest` | uuid, goal, targetAction 또는 targetTag, measureRaw, periodRaw(daily/weekly/monthly/custom), scheduleModeRaw(everyDay/weekdays/monthDays/ordinalWeekday), weekdays[], monthDays[], ordinalWeek/Weekday, customStart/End, targetSeconds/targetCount, directionRaw(atLeast/atMost), sortOrder | 꼬리표 대상이면 measure는 생성 시 선택, `targetActions`는 해당 measure의 행동만 | -| `DiaryEntry` | dayKey(논리적 하루 키), moodEmoji, moodImageData(externalStorage), **calendarEventIDs[]**(그날 타임테이블에 넣을 EKEvent id), **hiddenActionIDs[]**(그날 타임테이블에서 숨길 Action.uuid 문자열) | 날짜당 1개. 날짜별 선택은 uuid 문자열로 저장(기기 간 동기화 안전) | +| `DiaryEntry` | dayKey(논리적 하루 키), moodEmoji, moodImageData(externalStorage), **calendarEventIDs[]**(그날 타임테이블에 넣을 EKEvent id), **hiddenActionIDs[]**(그날 타임테이블에서 숨길 Action.uuid 문자열), **hiddenGoalIDs[]**(그날 '이 날의 목표' 카드에서 숨길 Goal.uuid 문자열) | 날짜당 1개. 날짜별 선택은 uuid 문자열로 저장(기기 간 동기화 안전) | | `DiaryTodo` | text, isDone, sortOrder | 요약 페이지 체크리스트 | | `DiaryPage` | index, lined, lineSpacing, drawingData(PKDrawing, externalStorage) | 768×1024 논리 좌표 | | `DiaryPageItem` | kind(photo/rectangle/ellipse/arrow/line/**text**), imageData, **text**(텍스트 상자 내용), colorHex, centerX/Y·widthRatio(비율 좌표), rotationDegrees | | @@ -192,7 +192,8 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" - 상세는 진입만 해도 엔트리를 생성(loadEntry)하므로, 달력 onAppear가 **완전히 빈 엔트리만 정리**(기분·할일·페이지 없음 + calendarEventIDs·hiddenActionIDs 비어있음 + 페이지 0개 — 캘린더/필터 선택이나 빈 노트 페이지가 있으면 보존) - 상세: 좌우 페이지 넘김. 1페이지=하루 정리 요약, 이후=펜슬 노트, 마지막=페이지 추가 자리 - **요약 페이지 섹션** (표시·순서는 "첫 화면 구성" 시트, 전 날짜 공통, standard defaults): 오늘 기분(대형 정사각 타일: 이모지/사진) · 요약 지표 · 이 날의 목표(그날 기준 다짐 현황, 주간/월간은 그날까지 누적) · 오늘 할 일 · 타임테이블 · 기록 상세 · 통계 막대. 화면 720pt 이상이면 타임테이블 좌측 고정 2컬럼 -- 타임테이블 카드 우상단 버튼: **필터**(그날 기록된 행동만 나열된 팝업에서 표시 선택, 날짜별 저장 `hiddenActionIDs`, 숨긴 개수 노란 배지 — 타임테이블에만 적용, 요약 수치·기록 목록은 전체 유지) + **캘린더**(그날의 EKEvent 선택, 날짜별 저장, 외곽선 스타일 블록). 둘 다 내보내기에 동일 반영 +- 타임테이블 카드 우상단 버튼: **필터**(그날 기록된 행동만 나열된 팝업에서 표시 선택, 날짜별 저장 `hiddenActionIDs`, 숨긴 개수 노란 배지 — 타임테이블에만 적용, 요약 수치·기록 목록은 전체 유지) + **캘린더**(그날의 EKEvent 선택, 날짜별 저장, **점선 테두리 + 채움 0.3 블록** — 행동 블록은 불투명 채움이라 스타일로 구분, 옅은 채움 0.14는 가시성 부족으로 폐기). 둘 다 내보내기에 동일 반영 +- '이 날의 목표' 카드 우상단 **목표 필터**: 그날 진행 중인 목표 중 표시할 것 선택(날짜별 저장 `hiddenGoalIDs`, 숨긴 개수 노란 배지, 열람 전용에선 버튼 숨김). 이미지 내보내기의 요약에는 목표 카드가 없으므로 내보내기 무영향 - **프리미엄 만료 열람 모드**: 이미 쓴 일기(hasContent)가 있으면 잠금 화면 대신 달력을 열람 전용으로 연다 — 배너 + 일기 있는 날짜만 활성, `environment(\.diaryReadOnly)`로 전 화면의 편집 UI(기분/할일/필기/필터/캘린더/⋯메뉴) 비활성, 열람 진입은 엔트리를 생성하지 않음. 일기를 쓴 적 없으면 기존 잠금 안내 - 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력, 배치 모드에서 '글 수정'으로 내용·색 편집). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`가 줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계)·사진·도형(사각/원/화살표/선) 배치 모드(드래그·핀치 크기 조절, 저장은 비율 좌표) @@ -307,7 +308,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" | 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan ` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) | | 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) | | 버블 | `-radialExpanded` `-radialDoubleTapTest`(2초 뒤 FAB 더블탭 재현 — `-settings.radialDoubleTapTab <탭>`과 함께) | -| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage ` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` | +| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage ` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` `-diaryHideFirstGoal` `-diaryShowGoalFilter` / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` | | 워치 | `-complicationPreview` `-complicationScroll` `-complicationScrollTo ` `-autoRunFirstAction` | --- diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index c2a257b..fbc22e3 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -3824,6 +3824,22 @@ } } }, + "모든 목표를 숨겼어요. 필터에서 다시 선택할 수 있어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All goals are hidden. You can choose them again in the filter." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "すべての目標を非表示にしました。フィルターで再度選択できます。" + } + } + } + }, "모음": { "localizations": { "en": { @@ -4273,6 +4289,22 @@ } } }, + "목표 필터": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Goal Filter" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "目標フィルター" + } + } + } + }, "목표 확인": { "localizations": { "en": { @@ -5271,6 +5303,22 @@ } } }, + "선택한 목표만 이 날짜의 '이 날의 목표' 카드에 표시돼요. 날짜마다 따로 선택할 수 있어요.": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Only the selected goals appear on this date's \"Goals of the Day\" card. You can choose separately for each date." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "選択した目標だけがこの日付の「この日の目標」カードに表示されます。日付ごとに別々に選択できます。" + } + } + } + }, "선택한 목표의 진행 현황이 모음 탭 상단에 표시돼요. 탭하면 목표 상세로 이동해요. 2개 이상 선택하면 카드를 옆으로 쓸어 넘겨 한 장씩 볼 수 있고, 표시 방식(다짐별로 각각 / 전체 다짐 합산)은 모든 카드에 똑같이 적용돼요.": { "localizations": { "en": { @@ -8586,18 +8634,18 @@ } } }, - "첫 페이지 타임테이블 카드 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 표시할 수 있어요. 필터 버튼을 누르면 그날 기록된 행동 중 타임테이블에 보여줄 것만 고를 수 있어요. 두 선택 모두 날짜마다 따로 저장되고, 일기를 내보낼 때도 그대로 반영돼요.": { + "첫 페이지 타임테이블 카드 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 표시할 수 있어요(일정은 점선 테두리로 구분돼요). 필터 버튼을 누르면 그날 기록된 행동 중 타임테이블에 보여줄 것만 고를 수 있고, '이 날의 목표' 카드에도 필터 버튼이 있어 보고 싶은 목표만 골라 둘 수 있어요. 선택은 모두 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 반영돼요.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Use the calendar button at the top right of the first page's timetable card to pick that day's calendar events to show alongside your records. The filter button lets you choose which of that day's recorded actions appear on the timetable. Both choices are saved per date and carry over to diary exports." + "value": "Use the calendar button at the top right of the first page's timetable card to pick that day's calendar events and show them together (events are marked with a dashed outline). Tap the filter button to choose which of that day's recorded actions appear on the timetable, and the \"Goals of the Day\" card also has a filter button so you can pick just the goals you want to see. All selections are saved per date, and the timetable selections carry over to diary exports." } }, "ja": { "stringUnit": { "state": "translated", - "value": "最初のページのタイムテーブルカード右上のカレンダーボタンで、その日のカレンダー予定を選んで一緒に表示できます。フィルターボタンを押すと、その日に記録した行動のうちタイムテーブルに表示するものだけを選べます。どちらの選択も日付ごとに保存され、日記の書き出しにもそのまま反映されます。" + "value": "最初のページのタイムテーブルカード右上のカレンダーボタンで、その日のカレンダー予定を選んで一緒に表示できます(予定は破線の枠で区別されます)。フィルターボタンを押すと、その日に記録した行動のうちタイムテーブルに表示するものを選べます。「この日の目標」カードにもフィルターボタンがあり、見たい目標だけを選んでおけます。選択はすべて日付ごとに保存され、タイムテーブルの選択は日記の書き出しにもそのまま反映されます。" } } } diff --git a/myApp/HaruDanim/IOS/Views/DiaryView.swift b/myApp/HaruDanim/IOS/Views/DiaryView.swift index a17228a..ae8a1db 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryView.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryView.swift @@ -134,14 +134,15 @@ struct DiaryRootView: View { } /// 완전히 빈 엔트리만 삭제. hasContent(기분·할 일·내용 있는 페이지)뿐 아니라 - /// 달력 마커에는 안 잡혀도 사용자에게 의미 있는 캘린더 일정·타임테이블 필터 선택이 - /// 있으면 보존하고, (아직 내용 없는) 노트 페이지를 만들어 둔 날도 보존한다. + /// 달력 마커에는 안 잡혀도 사용자에게 의미 있는 캘린더 일정·타임테이블 필터·목표 필터 + /// 선택이 있으면 보존하고, (아직 내용 없는) 노트 페이지를 만들어 둔 날도 보존한다. private func cleanupEmptyEntries() { var changed = false for entry in diaryEntries where !entry.hasContent && entry.calendarEventIDs.isEmpty && entry.hiddenActionIDs.isEmpty + && entry.hiddenGoalIDs.isEmpty && (entry.pagesStorage ?? []).isEmpty { context.delete(entry) changed = true @@ -823,6 +824,9 @@ private struct DiarySummaryPage: View { @State private var showingActionFilter = false @State private var filterExcluded: Set = [] + /// '이 날의 목표' 필터 팝업 — 선택은 entry.hiddenGoalIDs(uuid 문자열, 날짜별)로 즉시 저장 + @State private var showingGoalFilter = false + private var math: DayMath { DayMath() } /// 표시할 섹션 (구성 설정의 순서, 숨김 제외) @@ -854,6 +858,13 @@ private struct DiarySummaryPage: View { } } + /// 이 날짜에 숨기기로 한 목표를 뺀 표시 대상 (uuid 문자열 저장 — 기기 간 동기화에 안전) + private var visibleGoals: [Goal] { + guard !entry.hiddenGoalIDs.isEmpty else { return activeGoals } + let hidden = Set(entry.hiddenGoalIDs) + return activeGoals.filter { !hidden.contains($0.uuid.uuidString) } + } + /// 그날의 기록 스냅숏 (이미지 내보내기와 동일한 데이터 — 24시간 타임테이블 + 기록 목록) private var daySnapshot: ExportSnapshot { ExportBuilder.history( @@ -975,6 +986,9 @@ private struct DiarySummaryPage: View { excludedActionIDs: $filterExcluded ) } + .sheet(isPresented: $showingGoalFilter) { + DiaryGoalFilterSheet(entry: entry, goals: activeGoals) + } .onChange(of: filterExcluded) { _, newValue in // 시트에서 고르는 즉시 반영 + 날짜별로 저장 (uuid 문자열이라 기기 간 동기화에도 안전) let hidden = orderedActions @@ -1004,6 +1018,16 @@ private struct DiarySummaryPage: View { filterExcluded = timetableExcludedIDs showingActionFilter = true } + // 검증용: -diaryHideFirstGoal YES → 그날 첫 목표를 숨김 상태로 만들어 필터 동작 확인 + if UserDefaults.standard.bool(forKey: "diaryHideFirstGoal"), + entry.hiddenGoalIDs.isEmpty, + let first = activeGoals.first { + entry.hiddenGoalIDs = [first.uuid.uuidString] + } + // 검증용: -diaryShowGoalFilter YES → 목표 필터 팝업 바로 표시 + if UserDefaults.standard.bool(forKey: "diaryShowGoalFilter") { + showingGoalFilter = true + } } #endif } @@ -1018,7 +1042,10 @@ private struct DiarySummaryPage: View { ExportHeroRow(stats: daySnapshot.hero) case .goals: DiaryGoalsCard( - goals: activeGoals, + goals: visibleGoals, + totalCount: activeGoals.count, + hiddenCount: activeGoals.count - visibleGoals.count, + onFilter: (readOnly || activeGoals.isEmpty) ? nil : { showingGoalFilter = true }, dayKey: entry.dayKey, referenceNow: referenceNow, math: math @@ -1149,27 +1176,62 @@ private struct DiarySummaryPage: View { /// - 주간/월간/기간 다짐: 주기 시작부터 "그 날짜 끝"까지의 누적을 목표량과 비교 /// - '이하 유지' 다짐은 한도 대비 사용량 게이지(노랑, 초과 시 빨강)로 달성형과 구분 private struct DiaryGoalsCard: View { + /// 필터를 통과해 표시할 목표들 let goals: [Goal] + /// 이 날짜에 진행 중인 목표 전체 수 (숨긴 것 포함) + let totalCount: Int + /// 이 날짜에 숨긴 목표 수 (필터 버튼 배지) + let hiddenCount: Int + /// 필터 버튼 동작. nil = 버튼 숨김 (열람 전용이거나 진행 중 목표가 없을 때) + let onFilter: (() -> Void)? let dayKey: Date let referenceNow: Date let math: DayMath var body: some View { VStack(alignment: .leading, spacing: 12) { - HStack { + HStack(spacing: 8) { Text("이 날의 목표") .font(.subheadline.weight(.semibold)) Spacer() - Text(String(localized: "\(goals.count)개 진행 중")) + Text(String(localized: "\(totalCount)개 진행 중")) .font(.caption2) .foregroundStyle(.secondary) + if let onFilter { + // 타임테이블 필터 버튼과 같은 모양 — 숨긴 목표가 있으면 노란 배지 + Button(action: onFilter) { + HStack(spacing: 4) { + Image(systemName: "line.3.horizontal.decrease") + .font(.system(size: 12, weight: .semibold)) + if hiddenCount > 0 { + Text(verbatim: "\(hiddenCount)") + .font(.system(size: 11, weight: .bold).monospacedDigit()) + } + } + .foregroundStyle(hiddenCount > 0 ? AppTheme.yellow : AppTheme.green) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background( + (hiddenCount > 0 ? AppTheme.yellow : AppTheme.green).opacity(0.12), + in: Capsule() + ) + } + .buttonStyle(.plain) + .accessibilityLabel(Text("목표 필터")) + } } if goals.isEmpty { - Text("이 날짜에 진행 중인 목표가 없어요") - .font(.caption) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity) - .padding(.vertical, 14) + Group { + if totalCount == 0 { + Text("이 날짜에 진행 중인 목표가 없어요") + } else { + Text("모든 목표를 숨겼어요. 필터에서 다시 선택할 수 있어요.") + } + } + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) } else { ForEach(Array(goals.enumerated()), id: \.element.uuid) { index, goal in if index > 0 { @@ -1345,6 +1407,92 @@ private struct DiaryGoalsCard: View { } } +// MARK: - 이 날의 목표 필터 시트 + +/// '이 날의 목표' 카드에 표시할 목표를 고르는 시트. +/// 캘린더 일정 선택과 같은 패턴 — 전체 선택/해제 + 개별 체크, 선택은 이 날짜에만 적용된다. +private struct DiaryGoalFilterSheet: View { + @Bindable var entry: DiaryEntry + /// 이 날짜에 진행 중인 목표 전체 (숨긴 것도 나열해야 다시 켤 수 있다) + let goals: [Goal] + + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context + + private var hidden: Set { Set(entry.hiddenGoalIDs) } + private var allSelected: Bool { entry.hiddenGoalIDs.isEmpty } + + var body: some View { + NavigationStack { + List { + Section { + Button(allSelected ? String(localized: "전체 해제") : String(localized: "전체 선택")) { + save(allSelected ? goals.map(\.uuid.uuidString) : []) + } + .font(.callout.weight(.semibold)) + ForEach(goals, id: \.uuid) { goal in + goalRow(goal) + } + } footer: { + Text("선택한 목표만 이 날짜의 '이 날의 목표' 카드에 표시돼요. 날짜마다 따로 선택할 수 있어요.") + } + } + .scrollContentBackground(.hidden) + .background(AppTheme.background) + .navigationTitle("목표 필터") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("닫기") { dismiss() } + } + } + } + .presentationDetents([.medium, .large]) + } + + private func goalRow(_ goal: Goal) -> some View { + let id = goal.uuid.uuidString + let isOn = !hidden.contains(id) + return Button { + toggle(id) + } label: { + HStack(spacing: 10) { + Image(systemName: goal.symbolName) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 28, height: 28) + .background(goal.color, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) + Text(goal.title) + .font(.subheadline) + .foregroundStyle(.primary) + .lineLimit(1) + Spacer() + Image(systemName: isOn ? "checkmark.circle.fill" : "circle") + .font(.title3) + .foregroundStyle(isOn ? AppTheme.green : .secondary) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + private func toggle(_ id: String) { + var ids = entry.hiddenGoalIDs + if let index = ids.firstIndex(of: id) { + ids.remove(at: index) + } else { + ids.append(id) + } + save(ids) + } + + private func save(_ ids: [String]) { + entry.hiddenGoalIDs = ids + entry.updatedAt = .now + try? context.save() + } +} + // MARK: - 오늘 기분 카드 private struct DiaryMoodCard: View { diff --git a/myApp/HaruDanim/IOS/Views/ExportImageView.swift b/myApp/HaruDanim/IOS/Views/ExportImageView.swift index 4b35499..4e2984a 100644 --- a/myApp/HaruDanim/IOS/Views/ExportImageView.swift +++ b/myApp/HaruDanim/IOS/Views/ExportImageView.swift @@ -1174,20 +1174,23 @@ private struct ExportTimetableView: View { .offset(x: 2, y: y) } - /// 캘린더 일정 블록: 행동 블록과 구분되는 은은한 채움 + 외곽선 스타일 + /// 캘린더 일정 블록: 행동 블록(불투명 채움)과 **점선 테두리**로 구분한다. + /// 예전의 옅은 채움(0.14)+실선은 배경과 겹쳐 가시성이 너무 떨어졌다 — + /// 채움을 중간 농도(0.3)로 올려 눈에 띄게 하고, 구분은 색이 아니라 점선 윤곽이 담당한다. private func eventBlockView(_ block: ExportTimetableData.Block, columnWidth: CGFloat) -> some View { let y = CGFloat(block.startFrac - Double(data.hourLo)) * hourHeight let height = max(CGFloat(block.endFrac - block.startFrac) * hourHeight, 5) return RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(block.color.opacity(0.14)) + .fill(block.color.opacity(0.3)) .overlay( RoundedRectangle(cornerRadius: 4, style: .continuous) - .strokeBorder(block.color.opacity(0.55), lineWidth: 1) + .strokeBorder(block.color.opacity(0.9), + style: StrokeStyle(lineWidth: 1.5, dash: [4, 3])) ) .overlay(alignment: .topLeading) { if !weekly && height >= 18 { Label(block.name, systemImage: block.symbol) - .font(.caption2) + .font(.caption2.weight(.semibold)) .foregroundStyle(block.color) .lineLimit(1) .padding(4) diff --git a/myApp/HaruDanim/IOS/Views/HelpView.swift b/myApp/HaruDanim/IOS/Views/HelpView.swift index 67e0eb1..66b6c6a 100644 --- a/myApp/HaruDanim/IOS/Views/HelpView.swift +++ b/myApp/HaruDanim/IOS/Views/HelpView.swift @@ -201,7 +201,7 @@ struct HelpView: View { HelpTopic( symbol: "calendar.badge.plus", title: "타임테이블에 일정·필터", - body: "첫 페이지 타임테이블 카드 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 표시할 수 있어요. 필터 버튼을 누르면 그날 기록된 행동 중 타임테이블에 보여줄 것만 고를 수 있어요. 두 선택 모두 날짜마다 따로 저장되고, 일기를 내보낼 때도 그대로 반영돼요." + body: "첫 페이지 타임테이블 카드 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 표시할 수 있어요(일정은 점선 테두리로 구분돼요). 필터 버튼을 누르면 그날 기록된 행동 중 타임테이블에 보여줄 것만 고를 수 있고, '이 날의 목표' 카드에도 필터 버튼이 있어 보고 싶은 목표만 골라 둘 수 있어요. 선택은 모두 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 반영돼요." ), HelpTopic( symbol: "applepencil", diff --git a/myApp/HaruDanim/Shared/DiaryModels.swift b/myApp/HaruDanim/Shared/DiaryModels.swift index 878087d..58fdb37 100644 --- a/myApp/HaruDanim/Shared/DiaryModels.swift +++ b/myApp/HaruDanim/Shared/DiaryModels.swift @@ -28,6 +28,9 @@ final class DiaryEntry { /// 이 날짜의 타임테이블에서 숨길 행동 uuid 문자열들 (비어 있으면 전체 표시). /// 캘린더 일정과 마찬가지로 날짜마다 따로 저장하고, 내보내기에도 동일하게 반영. var hiddenActionIDs: [String] = [] + /// 이 날짜의 '이 날의 목표' 카드에서 숨길 목표 uuid 문자열들 (비어 있으면 전체 표시). + /// 기본값 있는 새 필드 — CloudKit 호환 규칙(§3)에 따라 스키마 추가만으로 안전. + var hiddenGoalIDs: [String] = [] var createdAt: Date = Date() var updatedAt: Date = Date()