feat(widget): fill quest list adaptively in goal progress widget

목표 진행률 위젯의 다짐 목록이 고정 3개(중형)/8개(대형) 제한이었던 것을
QuestBarsColumn으로 교체 — 셀의 실제 높이에 들어가는 만큼 최대한 표시
(행 14pt+간격 6pt 기준 계산)하고, 넘치는 개수는 헤더 오른쪽에 '+N' 표기.
기기·패밀리가 커지면 자동으로 더 보인다.

그 외 개수 제한 검토: 다짐 현황 위젯 그리드(4/8/16)는 스펙 고정, 통계
차트 시리즈(4/6)는 가독성 제한, 잠금화면 점(6)은 공간 제한이라 유지.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-13 15:14:26 +09:00
parent 5916e17b25
commit c6959950c2

View File

@ -181,35 +181,63 @@ struct QuestBarRow: View {
}
}
/// .
/// "+N" . (/ )
struct QuestBarsColumn: View {
let quests: [QuestCellSnapshot]
private let rowHeight: CGFloat = 14
private let rowSpacing: CGFloat = 6
private let headerHeight: CGFloat = 17
var body: some View {
GeometryReader { geo in
let fitCount = max(Int((geo.size.height - headerHeight + rowSpacing) / (rowHeight + rowSpacing)), 1)
let shown = Array(quests.prefix(fitCount))
VStack(alignment: .leading, spacing: rowSpacing) {
HStack {
Text("다짐 (하루)")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(.secondary)
Spacer()
if quests.count > shown.count {
Text(verbatim: "+\(quests.count - shown.count)")
.font(.system(size: 9, weight: .semibold).monospacedDigit())
.foregroundStyle(.tertiary)
}
}
.frame(height: headerHeight - rowSpacing, alignment: .top)
if quests.isEmpty {
Spacer()
Text("다짐이 없어요")
.font(.caption2)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
Spacer()
} else {
ForEach(shown) { quest in
QuestBarRow(quest: quest)
.frame(height: rowHeight)
}
Spacer(minLength: 0)
}
}
}
.padding(10)
.background(WidgetCellBackground())
}
}
/// 1 + ( 2 / 2)
struct GoalWithQuestsCellView: View {
let goal: GoalSnapshot
var questCount = 3
var body: some View {
HStack(alignment: .top, spacing: 10) {
GoalBarsCellView(goal: goal)
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 7) {
Text("다짐 (하루)")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(.secondary)
if goal.quests.isEmpty {
Spacer()
Text("다짐이 없어요")
.font(.caption2)
.foregroundStyle(.secondary)
Spacer()
} else {
ForEach(goal.quests.prefix(questCount)) { quest in
QuestBarRow(quest: quest)
}
Spacer(minLength: 0)
}
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(WidgetCellBackground())
QuestBarsColumn(quests: goal.quests)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
@ -258,7 +286,7 @@ struct GoalProgressWidgetView: View {
}
case .goalWithQuests:
if let goal = entry.goals.first ?? nil {
GoalWithQuestsCellView(goal: goal, questCount: 3)
GoalWithQuestsCellView(goal: goal)
}
}
}
@ -281,7 +309,7 @@ struct GoalProgressWidgetView: View {
VStack(spacing: 8) {
ForEach(0..<2, id: \.self) { index in
if index < entry.goals.count, let goal = entry.goals[index] {
GoalWithQuestsCellView(goal: goal, questCount: 3)
GoalWithQuestsCellView(goal: goal)
} else {
WidgetBlankCellView()
}
@ -292,24 +320,8 @@ struct GoalProgressWidgetView: View {
VStack(spacing: 8) {
GoalBarsCellView(goal: goal)
.frame(maxHeight: 130)
VStack(alignment: .leading, spacing: 7) {
Text("다짐 (하루)")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(.secondary)
if goal.quests.isEmpty {
Text("다짐이 없어요")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
ForEach(goal.quests.prefix(8)) { quest in
QuestBarRow(quest: quest)
}
}
Spacer(minLength: 0)
}
.padding(10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(WidgetCellBackground())
QuestBarsColumn(quests: goal.quests)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}