Skip to content

Commit 3480caa

Browse files
fix: 다이어리페이지 연동
1 parent 770cd0a commit 3480caa

3 files changed

Lines changed: 265 additions & 2 deletions

File tree

‎diary/urls.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,5 @@
9292
path("mypage/", TemplateView.as_view(template_name="diary/mypage.html"), name="mypage"),
9393
path("allnight-setup/", views.AllNightSetupView.as_view(), name="allnight-setup"),
9494
path("allnight-mode/", views.AllNightModeView.as_view(), name="allnight-mode"),
95-
path("mydiary/", TemplateView.as_view(template_name="diary/mydiary.html"), name="mydiary"),
95+
path("mydiary/", views.MyDiaryView.as_view(), name="mydiary"),
9696
]

‎diary/views.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,13 @@ class AllNightModeView(LoginRequiredMixin, TemplateView):
447447
template_name = "diary/allnight_mode.html"
448448
extra_context = {"page_title": "밤샘모드"}
449449

450+
451+
# 나의 다이어리 — /feed-status/, /sleep-logs/, /caffeine-logs/를 호출하므로 로그인 필수.
452+
class MyDiaryView(LoginRequiredMixin, TemplateView):
453+
login_url = "/auth/login"
454+
template_name = "diary/mydiary.html"
455+
extra_context = {"page_title": "나의 다이어리"}
456+
450457
# 수면 설문
451458
class SleepLogTimeView(LoginRequiredMixin, TemplateView):
452459
login_url = "/auth/login"

‎static/js/mydiary.js‎

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,257 @@
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

Comments
 (0)