1- //추후 연동
1+ /**
2+ * mydiary.js — "나의 다이어리" 화면(/mydiary/) 전용 스크립트.
3+ *
4+ * 카드 3개를 기존 API로 채운다(신규 백엔드 엔드포인트 없이 연결):
5+ * - 오늘의 카페인 : GET /feed-status/ (CALC-001 잔류량 그래프)
6+ * - 어제의 수면 : GET /sleep-logs/?page_size=1 (가장 최근 기록 = 전날 밤)
7+ * - 일주일 그래프 : GET /caffeine-logs/, /sleep-logs/ (최근 7일, 일별 집계는 프론트에서 계산)
8+ *
9+ * apiFetch/getCookie는 base.js(이 스크립트보다 먼저 로드됨)가 전역으로 제공한다.
10+ */
11+
12+ const CD_KST_TZ = 'Asia/Seoul' ;
13+ const CD_COLOR_LINE = '#543131' ; // --cd-brown-deep
14+ const CD_COLOR_LINE_SOFT = 'rgba(84, 49, 49, 0.15)' ;
15+ const CD_COLOR_MUTED = '#CABDB8' ; // --cd-brown-light
16+
17+ function cdFormatKstTime ( date ) {
18+ return date . toLocaleTimeString ( 'ko-KR' , {
19+ timeZone : CD_KST_TZ ,
20+ hour : '2-digit' ,
21+ minute : '2-digit' ,
22+ hour12 : false ,
23+ } ) ;
24+ }
25+
26+ // "YYYY-MM-DD" (Asia/Seoul 기준). caffeine-logs의 start_date/end_date 파라미터와
27+ // 같은 형식이라 그대로 쿼리스트링에 쓸 수 있다.
28+ function cdKstDateKey ( date ) {
29+ return date . toLocaleDateString ( 'en-CA' , { timeZone : CD_KST_TZ } ) ;
30+ }
31+
32+ function cdKstWeekdayLabel ( date ) {
33+ return date . toLocaleDateString ( 'ko-KR' , { timeZone : CD_KST_TZ , weekday : 'short' } ) ;
34+ }
35+
36+ /**
37+ * 오늘의 카페인 — FeedStatusView가 CALC-001로 계산해 내려주는 하루치 잔류량
38+ * 배열(graph.mg)을 선그래프로 그린다. 화면 로직은 feed.js의 loadCutoffTime과
39+ * 동일하게, 프로필이 없으면(404) 설문으로 보낸다.
40+ */
41+ async function loadTodayCaffeineChart ( ) {
42+ const canvas = document . getElementById ( 'todayCaffeineGraph' ) ;
43+ if ( ! canvas ) return ;
44+
45+ const res = await apiFetch ( '/feed-status/' ) ;
46+ if ( res . status === 404 ) {
47+ window . location . href = '/initial_survey/' ;
48+ return ;
49+ }
50+ if ( ! res . ok ) return ;
51+
52+ const data = await res . json ( ) ;
53+ const graph = data . graph ;
54+ if ( ! graph || ! graph . mg ?. length ) return ;
55+
56+ const start = new Date ( graph . start ) ;
57+ const labels = graph . mg . map ( ( _ , i ) =>
58+ cdFormatKstTime ( new Date ( start . getTime ( ) + i * graph . interval_min * 60000 ) )
59+ ) ;
60+
61+ const datasets = [
62+ {
63+ label : '체내 카페인(mg)' ,
64+ data : graph . mg ,
65+ borderColor : CD_COLOR_LINE ,
66+ backgroundColor : CD_COLOR_LINE_SOFT ,
67+ borderWidth : 2 ,
68+ pointRadius : 0 ,
69+ tension : 0.3 ,
70+ fill : true ,
71+ } ,
72+ ] ;
73+ if ( graph . threshold_mg != null ) {
74+ datasets . push ( {
75+ label : '임계치' ,
76+ data : graph . mg . map ( ( ) => graph . threshold_mg ) ,
77+ borderColor : CD_COLOR_MUTED ,
78+ borderWidth : 1 ,
79+ borderDash : [ 4 , 4 ] ,
80+ pointRadius : 0 ,
81+ fill : false ,
82+ } ) ;
83+ }
84+
85+ new Chart ( canvas , {
86+ type : 'line' ,
87+ data : { labels, datasets } ,
88+ options : {
89+ responsive : true ,
90+ maintainAspectRatio : false ,
91+ interaction : { intersect : false , mode : 'index' } ,
92+ plugins : {
93+ legend : { display : false } ,
94+ title : {
95+ display : true ,
96+ text : `오늘 총 ${ Math . round ( data . today_total_mg ?? 0 ) } mg` ,
97+ color : CD_COLOR_LINE ,
98+ font : { size : 11 } ,
99+ padding : { bottom : 4 } ,
100+ } ,
101+ } ,
102+ scales : {
103+ x : { ticks : { maxTicksLimit : 5 , font : { size : 9 } } , grid : { display : false } } ,
104+ y : { beginAtZero : true , ticks : { font : { size : 9 } } } ,
105+ } ,
106+ } ,
107+ } ) ;
108+ }
109+
110+ /**
111+ * 어제의 수면 — 가장 최근 SleepLog 1건(전날 밤 기록한 값)을 수면질(1~5) 막대로
112+ * 보여준다. 취침시각 잔류 카페인량이 함께 있으면 부제로 덧붙인다.
113+ */
114+ async function loadYesterdaySleepChart ( ) {
115+ const canvas = document . getElementById ( 'yesterdaySleepGraph' ) ;
116+ if ( ! canvas ) return ;
117+
118+ const res = await apiFetch ( '/sleep-logs/?page_size=1' ) ;
119+ if ( ! res . ok ) return ;
120+
121+ const data = await res . json ( ) ;
122+ const log = ( data . results || [ ] ) [ 0 ] ;
123+ const hasQuality = log ?. sleep_quality != null ;
124+ const quality = hasQuality ? log . sleep_quality : 0 ;
125+
126+ const subtitle = ! hasQuality
127+ ? '아직 기록된 수면 정보가 없어요'
128+ : log . residual_mg_at_sleep != null
129+ ? `수면질 ${ quality } /5 · 취침 시 잔류 ${ Math . round ( log . residual_mg_at_sleep ) } mg`
130+ : `수면질 ${ quality } /5` ;
131+
132+ new Chart ( canvas , {
133+ type : 'bar' ,
134+ data : {
135+ labels : [ '수면질' ] ,
136+ datasets : [
137+ {
138+ data : [ quality ] ,
139+ backgroundColor : hasQuality ? CD_COLOR_LINE : CD_COLOR_MUTED ,
140+ borderRadius : 6 ,
141+ barThickness : 24 ,
142+ } ,
143+ ] ,
144+ } ,
145+ options : {
146+ indexAxis : 'y' ,
147+ responsive : true ,
148+ maintainAspectRatio : false ,
149+ plugins : {
150+ legend : { display : false } ,
151+ title : { display : true , text : subtitle , color : CD_COLOR_LINE , font : { size : 11 } } ,
152+ } ,
153+ scales : {
154+ x : { min : 0 , max : 5 , ticks : { stepSize : 1 , font : { size : 9 } } } ,
155+ y : { display : false } ,
156+ } ,
157+ } ,
158+ } ) ;
159+ }
160+
161+ /**
162+ * 일주일 그래프 — 오늘을 포함한 최근 7일간의 하루 총 섭취량(mg)과 수면질(1~5)을
163+ * 이중축 꺾은선그래프로 겹쳐 보여준다. 둘 다 일별 합산 API가 없어 목록을 받아
164+ * 프론트에서 날짜(Asia/Seoul)별로 집계한다(개인 다이어리 기록량 규모라
165+ * page_size=100이면 충분하다). 수면 기록이 없는 날은 null로 둬 선을 끊는다
166+ * (0점으로 그리면 "최저 수면질"과 구분이 안 된다).
167+ */
168+ async function loadWeeklyChart ( ) {
169+ const canvas = document . getElementById ( 'weeklyGraph' ) ;
170+ if ( ! canvas ) return ;
171+
172+ const days = Array . from ( { length : 7 } , ( _ , i ) => new Date ( Date . now ( ) - ( 6 - i ) * 86400000 ) ) ;
173+ const startDate = cdKstDateKey ( days [ 0 ] ) ;
174+ const endDate = cdKstDateKey ( days [ 6 ] ) ;
175+
176+ const [ caffeineRes , sleepRes ] = await Promise . all ( [
177+ apiFetch ( `/caffeine-logs/?start_date=${ startDate } &end_date=${ endDate } &page_size=100` ) ,
178+ apiFetch ( `/sleep-logs/?start_date=${ startDate } &end_date=${ endDate } &page_size=100` ) ,
179+ ] ) ;
180+ if ( ! caffeineRes . ok ) return ;
181+
182+ const caffeineLogs = ( await caffeineRes . json ( ) ) . results || [ ] ;
183+ const mgByDate = Object . fromEntries ( days . map ( ( d ) => [ cdKstDateKey ( d ) , 0 ] ) ) ;
184+ caffeineLogs . forEach ( ( log ) => {
185+ const key = cdKstDateKey ( new Date ( log . created_at ) ) ;
186+ if ( key in mgByDate ) mgByDate [ key ] += log . caffeine_mg ;
187+ } ) ;
188+
189+ const qualityByDate = { } ;
190+ if ( sleepRes . ok ) {
191+ const sleepLogs = ( await sleepRes . json ( ) ) . results || [ ] ;
192+ sleepLogs . forEach ( ( log ) => {
193+ if ( log . sleep_quality == null ) return ;
194+ // 하루 1건 제한(SLEEP-002)이라 겹쳐 써도 안전하다.
195+ qualityByDate [ cdKstDateKey ( new Date ( log . created_at ) ) ] = log . sleep_quality ;
196+ } ) ;
197+ }
198+
199+ new Chart ( canvas , {
200+ type : 'line' ,
201+ data : {
202+ labels : days . map ( ( d ) => cdKstWeekdayLabel ( d ) ) ,
203+ datasets : [
204+ {
205+ label : '카페인(mg)' ,
206+ data : days . map ( ( d ) => Math . round ( mgByDate [ cdKstDateKey ( d ) ] ) ) ,
207+ borderColor : CD_COLOR_LINE ,
208+ backgroundColor : CD_COLOR_LINE_SOFT ,
209+ borderWidth : 2 ,
210+ pointRadius : 2 ,
211+ tension : 0.3 ,
212+ fill : true ,
213+ yAxisID : 'y' ,
214+ } ,
215+ {
216+ label : '수면질(/5)' ,
217+ data : days . map ( ( d ) => qualityByDate [ cdKstDateKey ( d ) ] ?? null ) ,
218+ borderColor : CD_COLOR_MUTED ,
219+ borderWidth : 2 ,
220+ borderDash : [ 4 , 4 ] ,
221+ pointRadius : 2 ,
222+ spanGaps : false ,
223+ tension : 0.3 ,
224+ fill : false ,
225+ yAxisID : 'y1' ,
226+ } ,
227+ ] ,
228+ } ,
229+ options : {
230+ responsive : true ,
231+ maintainAspectRatio : false ,
232+ interaction : { intersect : false , mode : 'index' } ,
233+ plugins : {
234+ legend : { display : true , labels : { boxWidth : 10 , font : { size : 9 } } } ,
235+ } ,
236+ scales : {
237+ x : { grid : { display : false } , ticks : { font : { size : 10 } } } ,
238+ y : {
239+ position : 'left' ,
240+ beginAtZero : true ,
241+ ticks : { font : { size : 9 } } ,
242+ } ,
243+ y1 : {
244+ position : 'right' ,
245+ min : 0 ,
246+ max : 5 ,
247+ ticks : { stepSize : 1 , font : { size : 9 } } ,
248+ grid : { drawOnChartArea : false } ,
249+ } ,
250+ } ,
251+ } ,
252+ } ) ;
253+ }
254+
255+ loadTodayCaffeineChart ( ) ;
256+ loadYesterdaySleepChart ( ) ;
257+ loadWeeklyChart ( ) ;
0 commit comments