Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
2 changes: 0 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -16,7 +15,6 @@ const queryClient = new QueryClient({
});

function App() {
const { isSignal, getSendAlarm } = useNotification();
return (
<ThemeProvider theme={defaultTheme}>
<SearchFilteringProvider>
Expand Down
13 changes: 13 additions & 0 deletions src/api/alarm.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ 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.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
70 changes: 70 additions & 0 deletions src/components/comment/DropDownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import useDeleteComment from '../../hooks/CommentHooks/useDeleteComment';
import useDeleteReply from '../../hooks/CommentHooks/useDeleteReply';
import { useModal } from '../../hooks/useModal';
import ReportModal from '../reportComponent/ReportModal';
import * as S from './DropDownItem.styled';

interface DropdownProps {
projectId: number;
activateEditMode?: number | null;
commentId: number;
recommentId?: number;
loginUserId?: number;
commentUserId: number;
reportTitle: { userImg: string; userName: string } | string;
reply?: boolean;
onEdit?: () => void;
}

const DropDownItem = ({
projectId,
onEdit,
activateEditMode,
commentId,
recommentId,
commentUserId,
loginUserId,
reply,
reportTitle,
}: DropdownProps) => {
const { removeComment } = useDeleteComment(projectId);
const { removeReply } = useDeleteReply(commentId, projectId);
const { isOpen, handleOpenReportModal, handleCloseReportModal } = useModal();

const onDelete = (commentId: number, recommentId?: number) => {
if (reply && recommentId) {
if (confirm('답글을 완성히 삭제할까요?')) removeReply(recommentId);
} else {
if (confirm('댓글을 완성히 삭제할까요?')) removeComment(commentId);
}
};

return (
<>
<S.Container>
<S.Item onClick={handleOpenReportModal}>신고하기</S.Item>

{loginUserId === commentUserId && (
<>
<S.Item onClick={onEdit}>
{activateEditMode === commentId ? '수정 취소하기' : '수정하기'}
</S.Item>
<S.Item onClick={() => onDelete(commentId, recommentId)}>
삭제하기
</S.Item>{' '}
</>
)}
</S.Container>
{isOpen && (
<ReportModal
reportTitle={reportTitle}
targetId={recommentId ? recommentId : commentId}
type={recommentId ? 'recomment' : 'comment'}
onClose={handleCloseReportModal}
/>
)}
</>
);
};

export default DropDownItem;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as S from './CommentComponentLayout.styled';
import DropDown from '../../common/dropDown/DropDown';
import DropDownItem from '../../common/dropDown/DropDownItem';
import DropDownItem from '../DropDownItem';
import { CommentType } from '../../../models/comment';
import dropdownButton from '../../../assets/dropdownButton.svg';
import useComment from '../../../hooks/CommentHooks/useComment';
Expand All @@ -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 Expand Up @@ -49,8 +53,3 @@ export const ErrorMessage = styled.div`
margin-left: 15px;
margin-bottom: 10px;
`;

export const Message = styled.p`
color: ${({ theme }) => theme.color.red};
font-size: 10px;
`;
11 changes: 5 additions & 6 deletions src/components/comment/replyComponent/ReplyComponent.styled.ts
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 Expand Up @@ -60,8 +64,3 @@ export const ErrorMessage = styled.div`
padding-left: 15px;
margin-bottom: 10px;
`;

export const Message = styled.p`
color: ${({ theme }) => theme.color.red};
font-size: 10px;
`;
3 changes: 2 additions & 1 deletion src/components/comment/replyComponent/ReplyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as S from './ReplyComponent.styled';
import DefaultImg from '../../../assets/defaultImg.png';
import useComment from '../../../hooks/CommentHooks/useComment';
import DropDown from '../../common/dropDown/DropDown';
import DropDownItem from '../../common/dropDown/DropDownItem';
import DropDownItem from '../DropDownItem';
import dropdownButton from '../../../assets/dropdownButton.svg';
import CommentInput from '../commentInput/CommentInput';
import useGetReply from '../../../hooks/CommentHooks/useGetReply';
Expand Down 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
45 changes: 45 additions & 0 deletions src/components/common/Toast/Toast.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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: ${({ theme }) => theme.buttonScheme.grey.color};
padding: 12px 20px;
border-radius: ${({ theme }) => theme.borderRadius.small};
animation: ${fadeInUp} 0.3s ease-out,
${({ $exiting }) => $exiting && fadeOut} 0.3s ease-in forwards;
`;

export const LiveMessage = styled.p`
color: ${({ theme }) => theme.color.white};
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

export const LiveDate = styled.p`
color: ${({ theme }) => theme.color.white};
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
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 '../../../context/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}
onRemove={() => onRemove(toast.id)}
/>
))}
</S.Container>,
document.body
);
};

export default ToastContainer;
37 changes: 37 additions & 0 deletions src/components/common/Toast/ToastItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react';
import * as S from './Toast.styled';
import { AlarmLive } from '../../../models/alarm';
import { Link } from 'react-router-dom';
import { routeSelector } from '../../../util/routeSelector';

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

const ToastItem = ({ content, duration, onRemove }: ToastItemProps) => {
const [exiting, setExiting] = useState(false);
const route = routeSelector(content.routingId, content.alarmFilterId);

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

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

return (
<Link to={route}>
<S.Item $exiting={exiting} onAnimationEnd={handleAnimationEnd}>
<S.LiveMessage>{content.message}</S.LiveMessage>
<S.LiveDate>{content.createAt}</S.LiveDate>
</S.Item>
</Link>
);
};

export default ToastItem;
Loading