Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
148b0b1
feat : 실시간 알림 데이터를 위한 Toast 띄우기
layout-SY Apr 26, 2025
90a8c62
Merge branch 'develop' of https://github.com/devpalsPlus/frontend int…
layout-SY Apr 26, 2025
bab50a1
Merge branch 'develop' of https://github.com/devpalsPlus/frontend int…
layout-SY Apr 27, 2025
3572213
style : 공고 상세 페이지 댓글 부분 하단에 여유 공간 추가
layout-SY Apr 27, 2025
3c34e1e
feat : 신고하기 구현(API 미적용)
layout-SY Apr 28, 2025
9abf6c2
fix : content 길이가 문단을 넘어가면 들여쓰기가 적용되지 않는 문제 해결
layout-SY Apr 28, 2025
645108b
feat : 댓글이 없을 경우 추가
layout-SY Apr 29, 2025
b3a79c1
feat : 유저, 댓글/대댓글 등에 따른 신고 인터페이스 변경
layout-SY Apr 29, 2025
0ad9c87
feat : 신고 후 모달 닫기
layout-SY Apr 29, 2025
02db193
feat : 헤더의 알림 조회 스크롤 추가
layout-SY Apr 29, 2025
c5727fa
feat : 토스트 구현
layout-SY Apr 29, 2025
80f0aae
fix : 빌드 에러 수정
layout-SY Apr 30, 2025
1d18d2f
refactor : 타입 정리
layout-SY Apr 30, 2025
ee8e78e
refactor : 코드 리뷰 사항 적용
layout-SY Apr 30, 2025
962152c
refactor : 파일 위치 변경
layout-SY Apr 30, 2025
bb0f4c2
feat : 실시간 알림 타입 토스트에 적용 및 토스트 알림에 route 추가
layout-SY Apr 30, 2025
2310dc8
feat : 헤더 전체 알림에 route 추가
layout-SY Apr 30, 2025
33e2a95
feat : 실시간 알림 타입 생성
layout-SY Apr 30, 2025
51065f6
refactor : 코드 리뷰 수정 사항 반영
layout-SY Apr 30, 2025
a9b6503
refactor : 형준님 리뷰 사항 반영
layout-SY Apr 30, 2025
aafb853
feat : 현재 시간과 비교하여 알림 지난 시간 표시
layout-SY Apr 30, 2025
eb35eac
refactor : 토스트 테스트 버튼 삭제
layout-SY Apr 30, 2025
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
7 changes: 4 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GlobalStyle } from './style/global';
import { defaultTheme } from './style/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SearchFilteringProvider } from './context/SearchFilteringContext';
import useNotification from './hooks/useNotification';
import { ToastProvider } from './components/common/Toast/ToastProvider';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -16,13 +16,14 @@ const queryClient = new QueryClient({
});

function App() {
const { isSignal, getSendAlarm } = useNotification();
return (
<ThemeProvider theme={defaultTheme}>
<SearchFilteringProvider>
<GlobalStyle />
<QueryClientProvider client={queryClient}>
<AppRoutes />
<ToastProvider>
<AppRoutes />
</ToastProvider>
</QueryClientProvider>
</SearchFilteringProvider>
</ThemeProvider>
Expand Down
12 changes: 12 additions & 0 deletions src/api/alarm.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ export const patchAlarm = async (id: number) => {
console.log(e);
}
};

export const testLiveAlarm = async () => {
try {
const response = await httpClient.get<ApiAlarmList>(
`/user/send-alarm?alarmFilter=0`
);

return response;
} catch (e) {
console.log(e);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

새 함수 testLiveAlarm의 목적을 명확히 해주세요

이 함수의 목적이 테스트인지 실제 기능인지가 명확하지 않습니다. 함수명이 "test"로 시작하는 것은 테스트 코드임을 암시하지만, 실제 기능으로 사용될 예정이라면 더 적절한 이름이 필요합니다.

또한 다른 함수들과 에러 처리 방식이 일관되지 않습니다:

  • getAlarmList는 에러를 다시 throw하지만 이 함수는 에러를 로깅만 합니다
  • 다른 함수들과 달리 에러 메시지가 없는 console.log를 사용합니다
export const testLiveAlarm = async () => {
  try {
    const response = await httpClient.get<ApiAlarmList>(
      `/user/send-alarm?alarmFilter=0`
    );

    return response;
  } catch (e) {
-    console.log(e);
+    console.error('라이브 알람 요청 에러:', e);
+    throw e;
  }
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const testLiveAlarm = async () => {
try {
const response = await httpClient.get<ApiAlarmList>(
`/user/send-alarm?alarmFilter=0`
);
return response;
} catch (e) {
console.log(e);
}
};
export const testLiveAlarm = async () => {
try {
const response = await httpClient.get<ApiAlarmList>(
`/user/send-alarm?alarmFilter=0`
);
return response;
} catch (e) {
console.error('라이브 알람 요청 에러:', e);
throw e;
}
};

2 changes: 1 addition & 1 deletion src/api/projectLists.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const getProjectLists = async (params: SearchFilters) => {
params,
paramsSerializer: { indexes: null },
});

return response.data.data;
} catch (e) {
console.log('getProjectLists', e);
Expand Down
1 change: 0 additions & 1 deletion src/api/reply.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const postReply = async (
commentId: number,
content: string
) => {
console.log(content);
try {
const response = await httpClient.post(
`/project/${projectId}/comment/${commentId}/recomment`,
Expand Down
15 changes: 15 additions & 0 deletions src/api/report.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiPostContent } from '../models/report';
import { httpClient } from './http.api';

export const postReport = async (formData: ApiPostContent) => {
try {
const response = await httpClient.post(`/report`, formData);
if (response.status !== 200) {
throw new Error(`${response.status}`);
}
return response.status;
} catch (error) {
console.error(error);
throw error;
}
};
8 changes: 7 additions & 1 deletion src/components/comment/CommentLayout.styled.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import styled from 'styled-components';

export const Container = styled.div``;
export const Container = styled.div`
padding-bottom: 100px;
`;

export const CommentCountsContainer = styled.div`
margin-bottom: 20px;
Expand All @@ -27,3 +29,7 @@ export const ReplyContainer = styled.div`
padding-left: 100px;
margin-top: 20px;
`;

export const ErrorMessage = styled.p`
color: #333;
`;
18 changes: 16 additions & 2 deletions src/components/comment/CommentLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,33 @@ const CommentLayout = ({
const { getCommentList, isLoading, isFetching, isError } =
useGetComment(projectId);

if (!getCommentList) {
return (
<S.Container>
<S.CommentCountsContainer>
<S.Count>댓글 없음</S.Count>
</S.CommentCountsContainer>
</S.Container>
);
}

if (isLoading || isFetching) {
return <LoadingSpinner />;
}

if (isError) {
console.error(isError);
return '댓글을 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요.';
return (
<S.ErrorMessage>
댓글을 불러오는 중 오류가 발생했습니다. 다시 시도해 주세요.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryErroBoundary 를 통해서 에러나는 경우 감지하여 FallbackUI를 보여주게 되는데, ErrorMessage를 작성하신 이유가 궁금합니다!

Copy link
Collaborator Author

@layout-SY layout-SY Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생각을 못했네요. 제외 하겠습니다.

</S.ErrorMessage>
);
}

return (
<S.Container>
<S.CommentCountsContainer>
<S.Count>댓글 {getCommentList?.length || 0}개</S.Count>
<S.Count>댓글 {getCommentList.length}개</S.Count>
</S.CommentCountsContainer>

<S.CommentInput>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import CommentComponent from './commentComponent/CommentComponent';

interface CommentLayoutProps {
projectId: number;
getCommentList: CommentType[] | undefined;
getCommentList: CommentType[];
createrId?: number;
loginUserId?: number | undefined;
}
Expand Down Expand Up @@ -62,6 +62,7 @@ const CommentComponentLayout = ({
onEdit={() => onEdit(item.id)}
loginUserId={loginUserId}
commentUserId={item.user.id}
reportTitle={item.content}
activateEditMode={activateEditMode}
/>
</DropDown>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export const NickName = styled.p`
`;

export const Comment = styled.span`
margin-left: 11px;
display: inline-block;
max-width: calc(100% - 12px);
word-break: break-word;
white-space: pre-wrap;
margin-left: 12px;
`;

export const ReplyInputButton = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export const NickName = styled.p`
`;

export const Comment = styled.span`
margin-left: 11px;
display: inline-block;
max-width: calc(100% - 12px);
word-break: break-word;
white-space: pre-wrap;
margin-left: 12px;
`;

export const ReplyContainer = styled.div`
Expand Down
1 change: 1 addition & 0 deletions src/components/comment/replyComponent/ReplyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const ReplyComponent = ({
onEdit={() => onEdit(item.id)}
loginUserId={loginUserId}
commentUserId={item.user.id}
reportTitle={item.content}
activateEditMode={activateEditMode}
reply={true}
/>
Expand Down
34 changes: 34 additions & 0 deletions src/components/common/Toast/Toast.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import styled, { keyframes } from 'styled-components';

const fadeInUp = keyframes`
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
`;
const fadeOut = keyframes`
from { opacity: 1; }
to { opacity: 0; }
`;

export const Container = styled.div`
position: fixed;
width: 330px;
bottom: 30px;
right: 30px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 1000;
`;

export const Item = styled.div<{ $exiting: boolean }>`
background-color: rgba(50, 50, 50, 0.9);
color: ${({ theme }) => theme.color.white};
padding: 12px 20px;
border-radius: 8px;
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
animation: ${fadeInUp} 0.3s ease-out,
${({ $exiting }) => $exiting && fadeOut} 0.3s ease-in forwards;
`;
26 changes: 26 additions & 0 deletions src/components/common/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createPortal } from 'react-dom';
import { ToastMessage } from './ToastContext';
import ToastItem from './ToastItem';
import * as S from './Toast.styled';

interface ToastContainerProps {
toasts: ToastMessage[];
onRemove: (id: string) => void;
}

const ToastContainer = ({ toasts, onRemove }: ToastContainerProps) => {
return createPortal(
<S.Container>
{toasts.map((toast) => (
<ToastItem
key={toast.id}
{...toast}
onDone={() => onRemove(toast.id)}
/>
))}
</S.Container>,
document.body
);
};

export default ToastContainer;
17 changes: 17 additions & 0 deletions src/components/common/Toast/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createContext } from 'react';

export interface ToastMessage {
id: string;
content: string;
duration: number;
}

export interface ToastContextProps {
addToast: (content: string, duration?: number) => void;
removeToast: (id: string) => void;
}

export const ToastContext = createContext<ToastContextProps>({
addToast: () => {},
removeToast: () => {},
});
30 changes: 30 additions & 0 deletions src/components/common/Toast/ToastItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';
import * as S from './Toast.styled';

interface ToastItemProps {
id: string;
content: string;
duration: number;
onDone: () => void;
}

const ToastItem = ({ content, duration, onDone }: ToastItemProps) => {
const [exiting, setExiting] = useState(false);

useEffect(() => {
const timer = setTimeout(() => setExiting(true), duration);
return () => clearTimeout(timer);
}, [duration]);

const handleAnimationEnd = () => {
if (exiting) onDone();
};

return (
<S.Item $exiting={exiting} onAnimationEnd={handleAnimationEnd}>
{content}
</S.Item>
);
};

export default ToastItem;
30 changes: 30 additions & 0 deletions src/components/common/Toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { ToastContext, ToastMessage } from './ToastContext';
import ToastContainer from './ToastContainer';

export const ToastProvider = ({ children }: PropsWithChildren) => {
const [toasts, setToasts] = useState<ToastMessage[]>([]);

const addToast = useCallback((content: string, duration = 4000) => {
const id = Date.now().toString() + Math.random();
setToasts((prev) => [...prev, { id, content, duration }]);
}, []);

const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);

useEffect(() => {
if (toasts.length >= 4) {
const oldestId = toasts[0].id;
removeToast(oldestId);
}
}, [toasts, removeToast]);

return (
<ToastContext.Provider value={{ addToast, removeToast }}>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
};
28 changes: 28 additions & 0 deletions src/components/common/Toast/ToastTestButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import styled from 'styled-components';
import { useToast } from '../../../hooks/useToast';

const TestButton = styled.button`
margin-left: 16px;
padding: 8px 12px;
background-color: ${({ theme }) => theme.color.primary};
color: #fff;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
&:hover {
opacity: 0.9;
}
`;

const ToastTestButton = () => {
const { showToast } = useToast();

const handleClick = () => {
showToast('🛎️ 이건 테스트 토스트 메시지입니다!', 4000);
};

return <TestButton onClick={handleClick}>토스트 확인</TestButton>;
};

export default ToastTestButton;
Loading