Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type VersionInfo = {

// 플랫폼 정보 (예시)
type PlatformInfo = {
// eslint-disable-next-line no-undef
os: NodeJS.Platform;
arch: string;
};
Expand Down Expand Up @@ -73,6 +74,15 @@ interface ElectronAPI {

// 시스템 테마 조회
getSystemTheme: () => Promise<SystemTheme>;

// 알림 API
notification: {
show: (
title: string,
body: string,
) => Promise<{ success: boolean; error?: string }>;
requestPermission: () => Promise<{ success: boolean; supported: boolean }>;
};
}

// Expose version number to renderer
Expand Down Expand Up @@ -170,5 +180,17 @@ const electronAPI: ElectronAPI = {
ipcRenderer.invoke('theme:getSystemTheme') as ReturnType<
ElectronAPI['getSystemTheme']
>,

// 알림 API
notification: {
show: (title: string, body: string) =>
ipcRenderer.invoke('notification:show', title, body) as ReturnType<
ElectronAPI['notification']['show']
>,
requestPermission: () =>
ipcRenderer.invoke('notification:requestPermission') as ReturnType<
ElectronAPI['notification']['requestPermission']
>,
},
};
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
41 changes: 31 additions & 10 deletions src/renderer/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';
import { Ref } from 'react';

interface RefreshResponse {
timestamp: string;
success: boolean;
data: {
accessToken: string;
refreshToken: string;
};
code: string;
message: string | null;
}

const api: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL as string,
Expand Down Expand Up @@ -28,28 +40,37 @@ api.interceptors.response.use(
_retry?: boolean;
}; // 무한 요청 방지

if (error.response?.status === 401 && !originalRequest._retry) {
/* 에러가 401,403일 때 토큰 갱신 */
if (
(error.response?.status === 401 || error.response?.status === 403) &&
!originalRequest._retry
) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');

const { data: newToken } = await axios.post<{
accessToken: string;
refreshToken: string;
}>(
const { data: newToken } = await axios.post<RefreshResponse>(
`${import.meta.env.VITE_BASE_URL}/auth/refresh`,
{ refreshToken },
{ withCredentials: true },
);

localStorage.setItem('accessToken', newToken.accessToken);
localStorage.setItem('refreshToken', newToken.refreshToken);
/* success가 false이거나 응답으로 온 데이터가 비었을 때 */
if (!newToken.success || !newToken.data) {
throw new Error('Refresh token expired');
}

/* 새로 발급 받은 토큰 저장 */
const newAccessToken = newToken.data.accessToken;
const newRefreshToken = newToken.data.refreshToken;

localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);

api.defaults.headers.common['Authorization'] =
`Bearer ${newToken.accessToken}`;
`Bearer ${newAccessToken}`;
if (originalRequest.headers) {
originalRequest.headers['Authorization'] =
`Bearer ${newToken.accessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
}

return api(originalRequest);
Expand Down
33 changes: 33 additions & 0 deletions src/renderer/src/api/dashboard/useAverageScoreQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import api from '../api';
import { AverageScoreResponse } from '../../types/dashboard/averageScore';

/**
* 평균 자세 점수 조회 API
* GET /dashboard/average-score
*/
const getAverageScore = async (): Promise<AverageScoreResponse> => {
const response = await api.get<AverageScoreResponse>(
'/dashboard/average-score',
);
const result = response.data;

if (!result.success) {
throw new Error(result.message || '평균 점수 조회 실패');
}

return result;
};

/**
* 평균 자세 점수 조회 query 훅
* @example
* const { data, isLoading, error } = useAverageScoreQuery();
* const score = data?.data.score;
*/
export const useAverageScoreQuery = () => {
return useQuery({
queryKey: ['averageScore'],
queryFn: getAverageScore,
});
};
33 changes: 33 additions & 0 deletions src/renderer/src/api/dashboard/useLevelQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import api from '../api';
import { LevelResponse } from '../../types/dashboard/level';

/**
* 레벨 도달 현황 조회 API
* GET /dashboard/level
*/
const getLevel = async (): Promise<LevelResponse> => {
const response = await api.get<LevelResponse>('/dashboard/level');
const result = response.data;

if (!result.success) {
throw new Error(result.message || '레벨 조회 실패');
}

return result;
};

/**
* 레벨 도달 현황 조회 query 훅
* @example
* const { data, isLoading, error } = useLevelQuery();
* const level = data?.data.level;
* const current = data?.data.current;
* const required = data?.data.required;
*/
export const useLevelQuery = () => {
return useQuery({
queryKey: ['level'],
queryFn: getLevel,
});
};
34 changes: 34 additions & 0 deletions src/renderer/src/api/dashboard/usePostureGraphQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useQuery } from '@tanstack/react-query';
import api from '../api';
import { PostureGraphResponse } from '../../types/dashboard/postureGraph';

/**
* 바른 자세 점수 그래프 조회 API (최근 31일)
* GET /dashboard/posture-graph
*/
const getPostureGraph = async (): Promise<PostureGraphResponse> => {
const response = await api.get<PostureGraphResponse>(
'/dashboard/posture-graph',
);
const result = response.data;

if (!result.success) {
throw new Error(result.message || '자세 그래프 조회 실패');
}

return result;
};

/**
* 바른 자세 점수 그래프 조회 query 훅
* @example
* const { data, isLoading, error } = usePostureGraphQuery();
* const points = data?.data.points;
* // points: { "2025-01-01": 85, "2025-01-02": 92, ... }
*/
export const usePostureGraphQuery = () => {
return useQuery({
queryKey: ['postureGraph'],
queryFn: getPostureGraph,
});
};
10 changes: 10 additions & 0 deletions src/renderer/src/api/login/useLoginMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const useLoginMutation = () => {
localStorage.setItem('accessToken', res.data.accessToken);
localStorage.setItem('refreshToken', res.data.refreshToken);

/* 사용자 정보 조회 후 이름 저장 */
try {
const userResponse = await api.get('/users/me');
if (userResponse.data.success && userResponse.data.data.name) {
localStorage.setItem('userName', userResponse.data.data.name);
}
} catch (error) {
console.error('사용자 정보 조회 실패:', error);
}

navigate('/onboarding/init');
},
onError: (error) => {
Expand Down
13 changes: 12 additions & 1 deletion src/renderer/src/api/session/useStopSessionMutation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../api';
import { SessionActionResponse } from '../../types/main/session';

Expand Down Expand Up @@ -29,6 +29,8 @@ const stopSession = async (
* stopSession(sessionId);
*/
export const useStopSessionMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: stopSession,
onSuccess: () => {
Expand All @@ -42,6 +44,15 @@ export const useStopSessionMutation = () => {

// sessionId를 localStorage에서 제거
localStorage.removeItem('sessionId');

// 평균 자세 점수 쿼리 갱신
queryClient.invalidateQueries({ queryKey: ['averageScore'] });

// 레벨 쿼리 갱신
queryClient.invalidateQueries({ queryKey: ['level'] });

// 자세 그래프 쿼리 갱신
queryClient.invalidateQueries({ queryKey: ['postureGraph'] });
},
onError: (error) => {
console.error('세션 중단 오류:', error);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/renderer/src/pages/Main/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useCameraStore } from '../../store/useCameraStore';
import { usePostureStore } from '../../store/usePostureStore';
import { MetricData } from '../../types/main/session';
import AttendacePanel from './components/AttendacePanel';
import AveragePosturePanel from './components/AveragePosturePanel';
import AveragePosturePanel from './components/AveragePosture/AveragePosturePanel';
import HighlightsPanel from './components/HighlightsPanel';
import MainHeader from './components/MainHeader';
import MiniRunningPanel from './components/MiniRunningPanel';
Expand Down
50 changes: 0 additions & 50 deletions src/renderer/src/pages/Main/components/AverageGraph/data.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { getColor } from '../../../../../utils/getColor';
import { MONTHLY_DATA, WEEKLY_DATA, type AverageGraphDatum } from '../data';
import { getColor } from '@utils/getColor';
import { usePostureGraphQuery } from '@api/dashboard/usePostureGraphQuery';

type AverageGraphDatum = {
periodLabel: string;
score: number;
};

{
/* 주간/월간 */
}
export type AverageGraphPeriod = 'weekly' | 'monthly';

type ChartConfig = {
Expand All @@ -21,6 +23,8 @@ export function useAverageGraphChart(activePeriod: AverageGraphPeriod) {
document.documentElement.classList.contains('dark'),
);

const { data: apiData } = usePostureGraphQuery();

/* html의 class 속성 변경될 때마다 콜백 실행(다크모드 감지) */
useEffect(() => {
const observer = new MutationObserver(() => {
Expand All @@ -42,7 +46,23 @@ export function useAverageGraphChart(activePeriod: AverageGraphPeriod) {
'#ffbf00',
);

const data = activePeriod === 'weekly' ? WEEKLY_DATA : MONTHLY_DATA;
/* API 데이터를 그래프 형식으로 변환 */
let data: AverageGraphDatum[] = [];
if (apiData?.data?.points) {
const points = apiData.data.points;
const sortedEntries = Object.entries(points).sort(([dateA], [dateB]) =>
dateA.localeCompare(dateB),
);

/* 주간: 최근 7일, 월간: 전체 31일 */
const slicedEntries =
activePeriod === 'weekly' ? sortedEntries.slice(-7) : sortedEntries;

data = slicedEntries.map(([date, score]) => ({
periodLabel: new Date(date).getDate().toString(),
score,
}));
}

/* 최댓값 100 */
const domainMax = 100;
Expand All @@ -58,7 +78,7 @@ export function useAverageGraphChart(activePeriod: AverageGraphPeriod) {
gridColor: gridColorValue,
yAxisTicks: ticks,
};
}, [activePeriod, isDark]);
}, [activePeriod, isDark, apiData]);

return chartConfig;
}
Loading