Skip to content

Commit

Permalink
Feature/#119 - 알림 추가, 확인 기능 구현, 로그인 상태 관리 추가 (#364)
Browse files Browse the repository at this point in the history
* ✨ feat: 알림 관련 스키마 및 api 정의

* ✨ feat: 서비스 워커 추가

* ✨ feat: 알림 구독 커스텀훅 및 input 값 설정

* 🐛 fix: 그래프, 주식 정보의 높이가 옆의 컴포넌트와 맞지 않는 문제 수정

* 🐛 fix: 소유 주식이 아니더라도 스크롤 허용

* 🐛 fix: alarm api 관련 스키마 구조 및 useQuery 옵션 변경

* ✨ feat: 마이페이지에 알림 정보 추가 및 리팩토링

* ✨ feat: staleTime, invalidateQueries 지정

* ✨ feat: 주식 상세페이지에 알림 컴포넌트 구현

* 🐛 fix: contexts 내부에 theme 폴더 생성 및 이동

* ✨ feat: context api로 로그인 상태 관리

* 🐛 fix: 로그인 상태 확인 api 호출, props drilling을 모두 context로 대체

* 🐛 fix: 안내 문구 말투 변경

* 🐛 fix: modal 내용이 줄바꿈 되도록 설정

* 🐛 fix: 문장 사이의 띄어쓰기 삭제

* 🐛 fix: tradingVolume string 타입으로 변경

* 🐛 fix: 알림 권한은 처음 한번만 요청하도록 설정

* 🐛 fix: 알림 추가 디자인 수정
  • Loading branch information
baegyeong authored Dec 4, 2024
1 parent 72b02f8 commit 40c439e
Show file tree
Hide file tree
Showing 41 changed files with 504 additions and 1,611 deletions.
10 changes: 10 additions & 0 deletions packages/frontend/public/serviceWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
self.addEventListener('push', (event) => {
const data = event.data
? event.data.json()
: { title: '알림', body: '내용 없음' };

self.registration.showNotification(data.title, {
body: data.body,
// icon: 'icon.png',
});
});
4 changes: 2 additions & 2 deletions packages/frontend/src/apis/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ instance.interceptors.response.use(
const status = error.response?.status;

if (status === 400) {
alert('잘못된 요청입니다.');
alert('잘못된 요청이에요.');
}

if (status === 403) {
alert('로그인이 필요합니다.');
alert('로그인 후 이용 가능해요.');
location.href = '/login';
}
return Promise.reject(error);
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/src/apis/queries/alarm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './schema';
export * from './useGetAlarm';
export * from './useGetStockAlarm';
export * from './usePostCreateAlarm';
export * from './usePostInitAlarm';
47 changes: 47 additions & 0 deletions packages/frontend/src/apis/queries/alarm/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from 'zod';

const KeysSchema = z.object({
p256dh: z.string(),
auth: z.string(),
});

export const PostInitAlarmRequestSchema = z.object({
endpoint: z.string(),
keys: KeysSchema,
});

export type PostInitAlarmRequest = z.infer<typeof PostInitAlarmRequestSchema>;

export const PostInitAlarmResponseSchema = z.object({
message: z.string(),
});

export type PostInitAlarmResponse = z.infer<typeof PostInitAlarmResponseSchema>;

export const PostCreateAlarmRequestSchema = z.object({
stockId: z.string(),
targetPrice: z.number().optional(),
targetVolume: z.number().optional(),
alarmDate: z.string().datetime(),
});

export type PostCreateAlarmRequest = z.infer<
typeof PostCreateAlarmRequestSchema
>;

export const AlarmInfoSchema = z.object({
alarmId: z.number(),
stockId: z.string(),
targetPrice: z.number().nullable(),
targetVolume: z.string().nullable(),
alarmDate: z.string().datetime(),
});

export const AlarmResponseSchema = z.array(AlarmInfoSchema);
export type AlarmResponse = z.infer<typeof AlarmResponseSchema>;

export const StockAlarmRequestSchema = z.object({
stockId: z.string(),
});

export type StockAlarmRequest = z.infer<typeof StockAlarmRequestSchema>;
17 changes: 17 additions & 0 deletions packages/frontend/src/apis/queries/alarm/useGetAlarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { type AlarmResponse, AlarmResponseSchema } from './schema';
import { get } from '@/apis/utils/get';

const getAlarm = () =>
get<AlarmResponse>({
schema: AlarmResponseSchema,
url: '/api/alarm/user',
});

export const useGetAlarm = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
return useQuery({
queryKey: ['getAlarm'],
queryFn: getAlarm,
enabled: isLoggedIn,
});
};
24 changes: 24 additions & 0 deletions packages/frontend/src/apis/queries/alarm/useGetStockAlarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import {
type AlarmResponse,
StockAlarmRequest,
AlarmResponseSchema,
} from './schema';
import { get } from '@/apis/utils/get';

const getStockAlarm = ({ stockId }: StockAlarmRequest) =>
get<AlarmResponse>({
schema: AlarmResponseSchema,
url: `/api/alarm/stock/${stockId}`,
});

export const useGetStockAlarm = ({
stockId,
isLoggedIn,
}: StockAlarmRequest & { isLoggedIn: boolean }) => {
return useQuery({
queryKey: ['getStockAlarm', stockId],
queryFn: () => getStockAlarm({ stockId }),
enabled: isLoggedIn,
});
};
35 changes: 35 additions & 0 deletions packages/frontend/src/apis/queries/alarm/usePostCreateAlarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
type PostCreateAlarmRequest,
type AlarmResponse,
AlarmInfoSchema,
} from './schema';
import { post } from '@/apis/utils/post';

const postCreateAlarm = ({
stockId,
targetPrice,
targetVolume,
alarmDate,
}: PostCreateAlarmRequest) =>
post<AlarmResponse>({
params: { stockId, targetPrice, targetVolume, alarmDate },
schema: AlarmInfoSchema,
url: '/api/alarm',
});

export const usePostCreateAlarm = () => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['createAlarm'],
mutationFn: ({
stockId,
targetPrice,
targetVolume,
alarmDate,
}: PostCreateAlarmRequest) =>
postCreateAlarm({ stockId, targetPrice, targetVolume, alarmDate }),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['getStockAlarm'] }),
});
};
20 changes: 20 additions & 0 deletions packages/frontend/src/apis/queries/alarm/usePostInitAlarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMutation } from '@tanstack/react-query';
import {
type PostInitAlarmResponse,
PostInitAlarmResponseSchema,
} from './schema';
import { post } from '@/apis/utils/post';

const postInitAlarm = (subscription: PushSubscription) =>
post<PostInitAlarmResponse>({
params: subscription,
schema: PostInitAlarmResponseSchema,
url: '/api/push/subscribe',
});

export const usePostInitAlarm = () => {
return useMutation({
mutationKey: ['initAlarm'],
mutationFn: (subscription: PushSubscription) => postInitAlarm(subscription),
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export const useGetLoginStatus = () => {
return useQuery({
queryKey: ['loginStatus'],
queryFn: getLoginStatus,
staleTime: 1000 * 60 * 5,
});
};
2 changes: 1 addition & 1 deletion packages/frontend/src/apis/queries/user/useGetUserTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const useGetUserTheme = () => {
return useQuery({
queryKey: ['userTheme'],
queryFn: getUserTheme,
staleTime: 1000 * 60 * 5,
staleTime: 1000 * 60 * 30,
select: (data) => data.theme,
});
};
2 changes: 1 addition & 1 deletion packages/frontend/src/components/layouts/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Alarm } from './alarm';
import { MenuList } from './MenuList';
import { Search } from './search';
import { BOTTOM_MENU_ITEMS, TOP_MENU_ITEMS } from '@/constants/menuItems';
import { ThemeContext } from '@/contexts/themeContext';
import { ThemeContext } from '@/contexts/theme';
import { useOutsideClick } from '@/hooks/useOutsideClick';
import { type MenuSection } from '@/types/menu';
import { cn } from '@/utils/cn';
Expand Down
21 changes: 6 additions & 15 deletions packages/frontend/src/components/ui/alarm/Alarm.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import Date from '@/assets/date.svg?react';
import Flag from '@/assets/flag.svg?react';
import Bell from '@/assets/small-bell.svg?react';

export interface AlarmProps {
goalPrice?: number;
method?: 'push' | 'email';
date?: string;
option: string;
goalPrice: number | string;
alarmDate: string;
}

export const Alarm = ({ goalPrice, method, date }: AlarmProps) => {
export const Alarm = ({ option, goalPrice, alarmDate }: AlarmProps) => {
return (
<article className="display-medium14 text-dark-gray bg-extra-light-gray flex flex-col gap-2 rounded-md p-4">
<span className="flex items-center gap-2">
<Flag />
목표가: {goalPrice?.toLocaleString()}
</span>
<span className="flex items-center gap-2">
<Bell />
{method === 'push' ? '웹 푸시' : '이메일'} 알림
{option}: {goalPrice?.toLocaleString()}
</span>
<span className="flex items-center gap-2">
<Date />
{date}
{alarmDate.slice(0, 10)}
</span>
<section>
<button className="mr-2">수정</button>
<button>삭제</button>
</section>
</article>
);
};
2 changes: 1 addition & 1 deletion packages/frontend/src/components/ui/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const Modal = ({ title, children, onClose, onConfirm }: ModalProps) => {
>
<section className="flex flex-col items-center gap-1">
<h2 className="display-bold20">{title}</h2>
<p>{children}</p>
<pre className="text-center">{children}</pre>
</section>
<section className="flex gap-3">
<Button onClick={onClose}>취소</Button>
Expand Down
15 changes: 11 additions & 4 deletions packages/frontend/src/constants/alarmOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export const ALARM_OPTIONS = [
{ id: 1, label: '목표가' },
{ id: 2, label: '등락률' },
{ id: 3, label: '거래가' },
export type AlarmOptionName = 'targetVolume' | 'targetPrice';

interface AlarmOption {
id: number;
name: AlarmOptionName;
label: string;
}

export const ALARM_OPTIONS: AlarmOption[] = [
{ id: 1, name: 'targetPrice', label: '목표가' },
{ id: 2, name: 'targetVolume', label: '거래가' },
];
6 changes: 3 additions & 3 deletions packages/frontend/src/constants/chatStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const UserStatus = {
export type ChatStatus = keyof typeof UserStatus;

export const chatPlaceholder: Record<ChatStatus, string> = {
NOT_AUTHENTICATED: '로그인 후 입력 가능합니다.',
NOT_OWNERSHIP: '주식 소유자만 입력 가능합니다.',
OWNERSHIP: '100자 이내로 입력 가능합니다.',
NOT_AUTHENTICATED: '로그인 후 입력 가능해요.',
NOT_OWNERSHIP: '주식 소유자만 입력 가능해요.',
OWNERSHIP: '100자 이내로 입력 가능해요.',
} as const;
6 changes: 3 additions & 3 deletions packages/frontend/src/constants/modalMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ export type ModalMessage = 'NOT_AUTHENTICATED' | 'NOT_OWNERSHIP' | 'OWNERSHIP';
export const modalMessage = {
NOT_AUTHENTICATED: {
label: '내 주식 추가',
message: '로그인 후 이용가능합니다. \n로그인하시겠습니까?',
message: '로그인 후 이용가능해요.\n로그인하시겠어요?',
},
NOT_OWNERSHIP: {
label: '내 주식 추가',
message: '이 주식을 소유하시겠습니까?',
message: '이 주식을 소유하시겠어요?',
},
OWNERSHIP: {
label: '내 주식 삭제',
message: '이 주식 소유를 취소하시겠습니까?',
message: '이 주식 소유를 취소하시겠어요?',
},
};
2 changes: 2 additions & 0 deletions packages/frontend/src/contexts/login/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './loginContext';
export * from './loginProvider';
12 changes: 12 additions & 0 deletions packages/frontend/src/contexts/login/loginContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createContext } from 'react';
import { GetLoginStatus } from '@/apis/queries/auth/schema';

interface LoginContextType extends Partial<GetLoginStatus> {
isLoggedIn: boolean;
}

export const LoginContext = createContext<LoginContextType>({
isLoggedIn: false,
nickname: '',
subName: '',
});
22 changes: 22 additions & 0 deletions packages/frontend/src/contexts/login/loginProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type ReactNode } from 'react';
import { LoginContext } from './loginContext';
import { useGetLoginStatus } from '@/apis/queries/auth';

interface LoginProviderProps {
children: ReactNode;
}

export const LoginProvider = ({ children }: LoginProviderProps) => {
const { data: loginStatus } = useGetLoginStatus();

if (!loginStatus) return;
const { message, nickname, subName } = loginStatus;

return (
<LoginContext.Provider
value={{ isLoggedIn: message === 'Authenticated', nickname, subName }}
>
{children}
</LoginContext.Provider>
);
};
2 changes: 2 additions & 0 deletions packages/frontend/src/contexts/theme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './themeContext';
export * from './themeProvider';
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { useState } from 'react';
import { useContext, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { LoginContext } from '../login';
import { ThemeContext } from './themeContext';
import { useGetLoginStatus } from '@/apis/queries/auth';
import {
GetUserTheme,
useGetUserTheme,
usePatchUserTheme,
} from '@/apis/queries/user';

export const ThemeProvider = () => {
const { data: loginStatus } = useGetLoginStatus();
const { isLoggedIn } = useContext(LoginContext);
const { data: userTheme } = useGetUserTheme();
const { mutate: updateTheme } = usePatchUserTheme();

const isAuthenticated = loginStatus?.message === 'Authenticated';
const initialTheme = isAuthenticated
? userTheme
: localStorage.getItem('theme');
const initialTheme = isLoggedIn ? userTheme : localStorage.getItem('theme');
const [theme, setTheme] = useState<GetUserTheme['theme']>(
initialTheme as GetUserTheme['theme'],
);
Expand All @@ -27,7 +24,7 @@ export const ThemeProvider = () => {
localStorage.setItem('theme', newTheme);
setTheme(newTheme);

if (isAuthenticated) {
if (isLoggedIn) {
updateTheme({ theme: newTheme });
}
};
Expand Down
Loading

0 comments on commit 40c439e

Please sign in to comment.