From ad9af22e9d56bc30be77f48fc5bfbaea1141bf72 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Thu, 9 Jul 2026 15:53:25 +0900 Subject: [PATCH] feat(stats): replace general averages with per-action and per-tag metrics - Remove unnecessary overall totals and general average metrics below weekly and monthly bar charts - Add detailed summary sections displaying the sum and average calculated for each specific action and tag - Ensure the new summary metrics dynamically update based on the selected filters --- .../UserInterfaceState.xcuserstate | Bin 22039 -> 22031 bytes myApp/Haru_Danim/IOS/Localizable.xcstrings | 9 + myApp/Haru_Danim/IOS/Views/StatsTabView.swift | 198 ++++++++++++++---- 3 files changed, 171 insertions(+), 36 deletions(-) diff --git a/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate index 00527a30604f1c2ff4143a19c82659719f0e2625..95637a0a0b55d012a5b69f4d96cd153495acf394 100644 GIT binary patch delta 6665 zcmZ`+2V9fK`+raFj%Wx;2niYRCO`-oZ#LNpA*kS9xavm5EiRl@lXKLzYKz8EZEFSG zTd~%5|&~amSY;Lu@>8~9p_*Ng9A7h2XRAO zfScmBxE(ISop4v&0}sN3@en)|m*Nq)43EVVaC!7WT$*bQUW?b^^>_o`h&SQQ_zS!b z@5f)`Z}7MHJA4A4#5MRwd=B5ncko?Yi|^t4_z`|wT>#@bXwRSzkH4yJmynSVS1#a1 z$4bX#fm&*3WKP5t$?DN}Sf9~DOG?XHl$7=_8_;#gz|xYYrF~1rm5i!RmW2SQt?nqV zMY0u8A)qe-k?Ps{2S5PD;0^)CNO>iw1^ps(q99uoUCJmxZFCniA~A=6cme`- zE4GUIj7lH)2^Pej$omA?>l(Kp`xM2a5-C1x0b5g&O4t&%BEUg_vl6y}Z3%D@;EiNO zN4PVi#|&Al4?TfO-T#0^+J*SJ(}_B*0C8C$cD#RRepmGZse^O&Zjf zeJhE2OiaoEI5@V#z?k1Y)^9ij^drE}I-VRYH#G>3gySh@1sny-;Al7oj)mh0$R!|1 zKpp`h0`duHxB^a~)Nmr41SeCa1T>04*_eQ41T>GHHSJft2WQ3BcpuIrpn!nFN;n&S zKtK}$nnt&o?ZO4{BT7;M7ZT8-0xlw;W%PwPUA6>Pu%qe;;L^ya1hlRM{os&DWz4s| zjLf_euA`V`a1~q)*TA&|u&A^npooC>%iwyr0d9nw2djh%<(7h5Kfkz4GK|rr4&z4fZCN}LU0(wS`HeK9L@N6_H<=ML=04k|8+(qX`%rtk)ige1L_!89ycd$Zd^2O9sz`aDFjT7(yo-m z9&t;T5+JS%t2kP&)0qAS7?q#_Y{u&STJLt`Qsi`ZF^GBgX#jy(jh z|9P*jjkpEzg2XrL&O-}hz|Kbt2$)Gg!mLX45n@Y#9a0{Hz7JZ0Vr2mCprw&=0zP2N z09D1x;KTnVaYA9IH`A^{>;4Z#KB9d|fJj!iz5_?~898w1z|!KhO%e>>NJGROHCxki zaed6Cn2I=d;`9ofK)~{U7>W5<@UIfVA}nSx!N~-ytiTciRz=(T^u-ExN=g{17jggX z2Ug;AcFtH|1%1Gi+#z_MSk9(Jc^G)IKNY?B2(GGp(Qbd z_IDV3Oh>@h8fqdn2{*!xJGLrnR9se4i5pRo8XOx@i1TBXeRLUOKWq8Mxn{U!Y{}-h z1p!|Wu)Pwu!mSDTl7OB6ESVr?$G68FaL2dUHAj_=99Y_)fF0450Yh|n09!lbw|WZr zt%_QXE-UUmq@?JzK^5+dyHJtNt%~Y?*Ze(4_qvgGb5GotWex6yi*awvZtuGZ*h9cq z1ngagOK?Bj9}gg49|8Lb_?m!kqV;pngkymWdmYHD&b{Bc%d%-}U34Ssq8pIk@c=vI z@6OS6o&Eii@8|y0Ij(Ma59<0L%<0FP7+WR=VF##`~{ z1Y9KG8UeQ={}5|C-bFE0_)EM4?4st`KmQoiDmO^ikdc%xLwO&c{1vFC#^1Ixy@iy8|Tzh=OLx)kndRZ=ZwQ#ec@pxRTz zsJ>J&HJ(zjZ5i9zDKF)qY$`L=hHCkGyTZRw2bV^?6o-;g85BcVS&Jammg-1#qk6zb zuo>*c-W&$8cZM-=9Gn2d?A>81dv93BYSIaK1zv+UAnzCUq;Q8lCESM(QN3u}CL$w) zd}uJ5hfbqsta!w#x;1WtyW`<_Bzp!QgU7L_@QEy`A}pciN9QyVhj-!Q_!@p1-yl9M zULPNbFN*IKKQMl9{LuJe@gw3V#7~T$93P3F5$-8;oxdX|S4`$W7rIxjEbrw}9J(+l6z?ZBPHt>kXxZs+dc z?&9v@?&Tih9_Ak99_OCqp5k8SUgO^2-s0Zo-sL{wKI1-5*pTo=!Z!(r5)LOEO{huu zA>qe_pLq3n8lIVF;n{dTUPE3JURz!}UVC15-Voj>-b7ybJ>C-DQeGu*8E*w|6>kl1 z9d83~6K@Oe0Phg*Fz+buIPWCy6z??e4DT%OJnv#6m1sxBD-2ZR@eH-!&{kA+W?LP^b%Iwo~b8kqEU z(zK+FNn4YCPP&zJC#g2+e$pSJ`Xa7~C*q63LXk)$7G;RkB8^BZ(u)|8Nn{bV5p@#{ z5=|DBiz-FSM4Lt1MLR^hM0-TviVlbli4Kd7imr-YBny+}$!W>Tb5K2I2&9qF5kK5+{o#VyRdzP8B=EF0n`K69>dWaY)=uTqN!w?$k^Cmbk09 zyLgazh`3ZdTs%n}78CJQak+S|c%FEHxJtZSyi&YcyiL4ayhFT8d`Ns!d{O+f_=@rGuqIrNg8nq@$#xrBkFcr0+>*O3S5lrSqf< zq}9?NrPrjlq_?Gar4OZ#rB9^KWLTCY6U#DWI+;Ocl$m8tnOo+S`DMAX{<1}~9kQ=w zCuG-TH)Qu@Ph{a2@;JFbo+;0go8&IJM_wRrE^jGsEpIRHDDN!qBJU+1CNGnJEMF$y zDc>tUBfl)aBEKeoAb&1@DgRUcN&yrN6bXt%g+P&{NLHjNvK2N(GesvwFGX)fUqwH~ z0L2K!D8*>SSjBk7+lobsZHlvsM~YXeg4Cqc*$U2W_l~Vjowc0 zqramM(%;iZ=(F@?`f8ZIPT!HG9U`Z4{4ex>9n#mZDAtyC#9lsctBX;hk(R%JtF z3*}qNZpt3YVr3s?iE^s4TsdF4P`OCCM7dO1sa&SqrM#rPqrxhVs(~s&m8cS^l2pkm ziAt)Ht5Q|8N~Q9t0;-@YU)4xeplYgWu4<)fs|vSQby9UvbyM|FjaMyDRjIyLJxYh^ z@^n{v^Yq^7^V3(R?@2$9UX%Vq`j6>9rJqZ`kbWusPI_(n{q%nVFfBGiPSb%UqE8QRd>zPcoNhuFPDMxju7K=9bKh>J)Wz zb!T;Vb$ySG`~Tjru$FLG@wvG4%;`jrz3ujQXtl zX_h$4lhrjVoV7gbMAmN_k;bNJt?8`ks_CxjsTrUdp&6watr@EsuX$TDNfXx0(#+9B zHS;x#HJ@lc(^P2oXs&4PX&!5yXr5{Q(7eh9*>TxP+2ZVsY+W{!ZO*o3d$RrE?A+{7 zcEju;*|W31%08HVI{Q}k?d-=|N-NTewY1itWwb`ES?kw^v<^9o8Mu9n+oHUD93AUDy4h`&IW+5A{gT z(F^qweU3h)&(}B9H`ce)_tY2b`{+yb1N4LRL-gbI6ZK*J6#aDlyZV{>+4{x$PxMRl z75c{p${;qR7-R;8A_-W-_yx51A#*T4ocoh54M>!R%u8FngKs zapnT^n0dlHW1cfFnLn9VMqrFHB4fOylt#CJ}^Er zzA*l2d}RWr2Bs8~-o%(3VN<@TgQ=6LuW6uZh^f>x$~49_&NRXFo@th8k!gi#wP~Gc zqiKujbJKRyx2A)p?@dQdCrqbIr%h)}cgvZe8 z*7Mfu)`!-o*5}qgY`_L>@wWOlp-pN_x2bK}Hk~cU*34F98)chdTWs58+ilxp`^vV@ zcFcC%b|P%6vHfg&YI|<`!}iKv&yMUIJJ+6Q7uu8UDRz_HYPZ`Rc9-3257_hU`SwQk zLVHttbNc}M4EtL9QTxN3l$`vWemT=~mgVfr`99}p&WW6wobx%?b8hC`%DJ6$H|Ji? zO9$nMb6^M8k?0UQL=KCi$kEGD;uzo<+&R@b-8t78b z?Y!yy)p^%>-}%V-#QD4PrSmTr-<9MNyQD6Ki*}{E)UIro&Si7iT~1ffl^1q3boFr! zca3(9b&YpTb`jS!*E_DLYmIB2YlCZ(Yl~~EYnyAkYlmx>YmaNM>x}E1>w@c&>$2;b z>!#~h*Bw`_>w)W$>o?b*Zpt0!M(#v+id*hZbF18$?ksnTcm~ z+#O!yUgzHE-t6A%{@Q)Qea3yxebIf{{fqmyyViZ*{m}iw6Yr6G zQaxE7o2Rj-(9_;i?CI<2?-}G7>KX1C<%xKvdZv5c_006l_LO_(dOr57@~ro4@>F}a zdv<#Ec=mdZdd_)%@!a;*dLDQld!BgfdHLRCuf!|!(q2{Ao9WH+=6FNiX5N09nw>09mF=G*Pt>-*aGo$sjcgzuE^N8eAr=YFfdi@(2rh<})W zq<^e`f`5`f?2r1F_*eQj`nUUc`oHlX_Mh;d^jk;N>r#EoEQuT zNpM|l9tZg5`kv*5Pi@!-|q^Ss17X`UsoXharfm}UUvlF~e*gdg delta 6722 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 diff --git a/myApp/Haru_Danim/IOS/Localizable.xcstrings b/myApp/Haru_Danim/IOS/Localizable.xcstrings index af0e114..663cb93 100644 --- a/myApp/Haru_Danim/IOS/Localizable.xcstrings +++ b/myApp/Haru_Danim/IOS/Localizable.xcstrings @@ -521,6 +521,9 @@ }, "주 시작 요일" : { + }, + "주 평균" : { + }, "주기마다 목표량 이상을 달성하는 것이 목표예요." : { @@ -623,6 +626,9 @@ }, "하루 시작 시간을 걸치는 기록은 통계에서 자동으로 날짜별로 나누어 계산돼요." : { + }, + "하루 평균" : { + }, "하루다님" : { "comment" : "앱 이름", @@ -643,6 +649,9 @@ }, "한 줄에 표시할 개수" : { + }, + "합계" : { + }, "해제한 꼬리표에만 속한 행동은 통계에서 제외돼요." : { diff --git a/myApp/Haru_Danim/IOS/Views/StatsTabView.swift b/myApp/Haru_Danim/IOS/Views/StatsTabView.swift index 718d0ab..29e5568 100644 --- a/myApp/Haru_Danim/IOS/Views/StatsTabView.swift +++ b/myApp/Haru_Danim/IOS/Views/StatsTabView.swift @@ -4,8 +4,8 @@ // // 통계 탭 (독립 탭): 하루/주간/월간 차트 + 꼬리표·행동 다중 선택 필터 // - 하루: 꼬리표별 시간/횟수 가로 막대 (비율 비교) -// - 주간: 행동별 일별 꺾은선(태그 색 + 범례) + 주간 합계 막대 -// - 월간: 행동별 주차별/일별 꺾은선 + 월간 합계 막대 +// - 주간: 행동별 일별 꺾은선(태그 색 + 범례) + 주간 합계 막대 + 행동·꼬리표별 합계/평균 표 +// - 월간: 행동별 주차별/일별 꺾은선 + 월간 합계 막대 + 행동·꼬리표별 합계/평균 표 // import SwiftUI @@ -60,18 +60,33 @@ struct StatsTabView: View { filterChip } - ScrollView { - VStack(alignment: .leading, spacing: 16) { - switch span { - case .day: - dayCharts - case .week: - weekCharts - case .month: - monthCharts + ScrollViewReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 16) { + switch span { + case .day: + dayCharts + case .week: + weekCharts + case .month: + monthCharts + } } + .padding() + Color.clear + .frame(height: 1) + .id("statsBottom") + } + .onAppear { + #if DEBUG + // 검증용: -statScrollBottom YES면 하단 요약 카드까지 자동 스크롤 + if UserDefaults.standard.bool(forKey: "statScrollBottom") { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation { proxy.scrollTo("statsBottom", anchor: .bottom) } + } + } + #endif } - .padding() } } .background(AppTheme.background) @@ -360,6 +375,16 @@ struct StatsTabView: View { dailyLineCard("행동별 횟수 (일별)", type: .count, weekdayAxis: true) totalsBarCard("주간 시간 합계", type: .time) totalsBarCard("주간 횟수 합계", type: .count) + summaryTableCard( + "행동별 합계·평균", + timeRows: actionSummaryRows(for: .time), + countRows: actionSummaryRows(for: .count) + ) + summaryTableCard( + "꼬리표별 합계·평균", + timeRows: tagSummaryRows(for: .time), + countRows: tagSummaryRows(for: .count) + ) } // MARK: 월간 — 행동별 주차별/일별 꺾은선 + 월간 합계 막대 @@ -372,6 +397,16 @@ struct StatsTabView: View { dailyLineCard("행동별 횟수 (일별)", type: .count, weekdayAxis: false) totalsBarCard("월간 시간 합계", type: .time) totalsBarCard("월간 횟수 합계", type: .count) + summaryTableCard( + "행동별 합계·평균", + timeRows: actionSummaryRows(for: .time), + countRows: actionSummaryRows(for: .count) + ) + summaryTableCard( + "꼬리표별 합계·평균", + timeRows: tagSummaryRows(for: .time), + countRows: tagSummaryRows(for: .count) + ) } // MARK: 행동별 시리즈 계산 @@ -546,7 +581,7 @@ struct StatsTabView: View { } } - // MARK: 합계 막대 카드 (기간 누적 비율 비교 + 합계·하루 평균) + // MARK: 합계 막대 카드 (기간 누적 비율 비교) @ViewBuilder private func totalsBarCard(_ title: String, type: TrackingType) -> some View { @@ -570,34 +605,125 @@ struct StatsTabView: View { } .chartXAxisLabel(type == .time ? "시간(h)" : "횟수") .frame(height: CGFloat(items.count) * 40 + 30) - - let total = items.reduce(0.0) { $0 + $1.value } - let average = total / Double(elapsedDayCount) - HStack(spacing: 0) { - summaryCell(label: "\(span.label) 합계", value: valueLabel(total, type: type)) - Divider().frame(height: 28) - summaryCell( - label: "하루 평균", - value: type == .time - ? Format.durationShort(average) - : String(format: "%.1f회", average) - ) - } - .padding(.top, 4) } } } - private func summaryCell(label: String, value: String) -> some View { - VStack(spacing: 2) { - Text(label) - .font(.caption2) - .foregroundStyle(.secondary) - Text(value) - .font(.subheadline.weight(.semibold).monospacedDigit()) - .foregroundStyle(AppTheme.green) + // MARK: 행동·꼬리표별 합계/평균 요약 표 (주간/월간, 필터 반영) + + private struct SummaryRowItem: Identifiable { + let name: String + let color: Color + let symbol: String + /// 시간형은 초, 횟수형은 횟수 + let total: Double + let type: TrackingType + + var id: String { "\(type == .time ? "time" : "count")-\(name)" } + } + + /// 평균 계산용: 월 안에서 이미 시작된 주 수 (미래 주로 평균이 희석되지 않게) + private var elapsedWeekCount: Int { + max(weekRangesInMonth.filter { $0.lowerBound <= .now }.count, 1) + } + + private func actionSummaryRows(for type: TrackingType) -> [SummaryRowItem] { + let agg = Aggregator(math: math) + return activeActions(for: type) + .map { action in + let total: Double = switch type { + case .time: agg.seconds(for: action, in: spanRange) + case .count: Double(agg.count(for: action, in: spanRange)) + } + return SummaryRowItem(name: action.name, color: action.color, symbol: action.symbolName, total: total, type: type) + } + .sorted { $0.total > $1.total } + } + + private func tagSummaryRows(for type: TrackingType) -> [SummaryRowItem] { + let stats = tagStats(in: spanRange) + let rows: [SummaryRowItem] = switch type { + case .time: + stats.filter { $0.seconds > 0 } + .map { SummaryRowItem(name: $0.name, color: $0.color, symbol: "tag.fill", total: $0.seconds, type: .time) } + case .count: + stats.filter { $0.count > 0 } + .map { SummaryRowItem(name: $0.name, color: $0.color, symbol: "tag.fill", total: Double($0.count), type: .count) } + } + return rows.sorted { $0.total > $1.total } + } + + private func averageLabel(_ value: Double, type: TrackingType) -> String { + switch type { + case .time: return Format.durationShort(value) + case .count: return String(format: "%.1f회", value) + } + } + + @ViewBuilder + private func summaryTableCard(_ title: String, timeRows: [SummaryRowItem], countRows: [SummaryRowItem]) -> some View { + chartCard(title) { + if timeRows.isEmpty && countRows.isEmpty { + emptyChartText + } else { + VStack(alignment: .leading, spacing: 14) { + if !timeRows.isEmpty { + summaryGrid(sectionLabel: "시간", rows: timeRows) + } + if !timeRows.isEmpty && !countRows.isEmpty { + Divider() + } + if !countRows.isEmpty { + summaryGrid(sectionLabel: "횟수", rows: countRows) + } + } + } + } + } + + private func summaryGrid(sectionLabel: String, rows: [SummaryRowItem]) -> some View { + Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 12) { + GridRow { + Text(sectionLabel) + .fontWeight(.semibold) + Text("합계") + .gridColumnAlignment(.trailing) + Text("하루 평균") + .gridColumnAlignment(.trailing) + if span == .month { + Text("주 평균") + .gridColumnAlignment(.trailing) + } + } + .font(.caption2) + .foregroundStyle(.secondary) + ForEach(rows) { row in + GridRow { + HStack(spacing: 8) { + Image(systemName: row.symbol) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 24, height: 24) + .background(row.color, in: RoundedRectangle(cornerRadius: 7, style: .continuous)) + Text(row.name) + .font(.footnote) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + Text(valueLabel(row.total, type: row.type)) + .font(.footnote.weight(.semibold).monospacedDigit()) + .foregroundStyle(AppTheme.green) + Text(averageLabel(row.total / Double(elapsedDayCount), type: row.type)) + .font(.footnote.monospacedDigit()) + .foregroundStyle(.secondary) + if span == .month { + Text(averageLabel(row.total / Double(elapsedWeekCount), type: row.type)) + .font(.footnote.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + } } - .frame(maxWidth: .infinity) } // MARK: 공통