⚠️ 그룹을 불러오는 중 문제가 발생했습니다. 다시 시도해주세요.
}
diff --git a/src/components/atoms/share-button/index.tsx b/src/components/atoms/share-button/index.tsx
index ff2874a9..6e5db4f1 100644
--- a/src/components/atoms/share-button/index.tsx
+++ b/src/components/atoms/share-button/index.tsx
@@ -9,9 +9,9 @@ export const ShareButton = () => {
const shareButtonClickHandler = async () => {
try {
await navigator.clipboard.writeText(currentUrl);
- toast.success('클립보드에 복사되었습니다');
+ toast.success('링크가 복사되었습니다.');
} catch {
- toast.error('클립보드 복사 실패!');
+ toast.error('링크 복사에 실패하였습니다.');
}
};
diff --git a/src/components/error-fallback/index.tsx b/src/components/error-fallback/index.tsx
index 796f58c3..ec5ce68a 100644
--- a/src/components/error-fallback/index.tsx
+++ b/src/components/error-fallback/index.tsx
@@ -1,8 +1,9 @@
'use client';
-import { ReactNode } from "react";
+import useAuthStore from '@/stores/useAuthStore';
import { useRouter } from 'next/navigation';
-import useAuthStore from "@/stores/useAuthStore";
+import { ReactNode } from 'react';
+import { Button } from '../ui/button';
interface ErrorFallbackProps {
error: Error | null;
@@ -15,38 +16,48 @@ interface ErrorFallbackProps {
* @param error 에러 객체
* @param resetErrorBoundary 에러 재시도 함수
* @param children 표시할 메시지
- * @returns
+ * @returns
*/
-export const ErrorFallback = ({ error, resetErrorBoundary, children }: ErrorFallbackProps) => {
- const {clearUser} = useAuthStore();
-
+export const ErrorFallback = ({
+ error,
+ resetErrorBoundary,
+ children,
+}: ErrorFallbackProps) => {
+ const { clearUser } = useAuthStore();
+
const router = useRouter();
const handleClick = () => {
- if (error?.message.includes('401') || error?.message.toLowerCase().includes('unauthorized')) {
+ if (
+ error?.message.includes('401') ||
+ error?.message.toLowerCase().includes('unauthorized')
+ ) {
clearUser();
console.log('401 에러 발생');
- router.push('/login'); // 401 에러면 로그인 페이지로 이동
+ router.push('/login'); // 401 에러면 로그인 페이지로 이동
} else if (error?.message.includes('Network')) {
- resetErrorBoundary(); // 네트워크 에러면 재시도
+ resetErrorBoundary(); // 네트워크 에러면 재시도
} else {
- resetErrorBoundary(); // 기타 에러는 기본적으로 재시도
+ resetErrorBoundary(); // 기타 에러는 기본적으로 재시도
}
};
return (
-
+
{/* 에러 메시지 */}
-
{children}
+
{children}
{/* 에러 객체가 존재하는 경우 에러 상세 메시지 */}
{/* {error &&
{error.message}} */}
{/* resetErrorBoundary이 존재하는 경우 재시도 버튼 */}
-
+ {error?.message.includes('401') ||
+ error?.message.toLowerCase().includes('unauthorized')
+ ? '로그인하기'
+ : '다시 시도'}
+
);
-};
\ No newline at end of file
+};
diff --git a/src/components/organisms/reply/reply-section.tsx b/src/components/organisms/reply/reply-section.tsx
deleted file mode 100644
index ceb39595..00000000
--- a/src/components/organisms/reply/reply-section.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ReplyForm } from '@/components/molecules/reply/reply-form';
-import { QueryErrorBoundary } from '@/components/query-error-boundary';
-import { ReplyList } from './reply-list';
-
-export const ReplySection = () => {
- return (
- <>
-
-
- 댓글을 불러오는 중 문제가 발생했습니다.
-
- }
- >
-
-
- >
- );
-};
diff --git a/src/features/bookmark/components/bookmark-button-container.tsx b/src/features/bookmark/components/bookmark-button-container.tsx
index ab32a444..0ee8ce43 100644
--- a/src/features/bookmark/components/bookmark-button-container.tsx
+++ b/src/features/bookmark/components/bookmark-button-container.tsx
@@ -1,7 +1,7 @@
'use client';
import { request } from '@/api/request';
-import { BookmarkButton } from '@/components/atoms/bookmark-button';
+import { BookmarkButton } from '@/features/bookmark/components/bookmark-button';
import useAuthStore from '@/stores/useAuthStore';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
diff --git a/src/components/atoms/bookmark-button/index.tsx b/src/features/bookmark/components/bookmark-button.tsx
similarity index 100%
rename from src/components/atoms/bookmark-button/index.tsx
rename to src/features/bookmark/components/bookmark-button.tsx
diff --git a/src/components/atoms/apply-join-button.tsx/index.tsx b/src/features/group/components/apply-join-button.tsx
similarity index 92%
rename from src/components/atoms/apply-join-button.tsx/index.tsx
rename to src/features/group/components/apply-join-button.tsx
index 8eecf891..46628a5b 100644
--- a/src/components/atoms/apply-join-button.tsx/index.tsx
+++ b/src/features/group/components/apply-join-button.tsx
@@ -1,10 +1,10 @@
'use client';
import { request } from '@/api/request';
+import { LoginRequireButton } from '@/components/atoms/login-require-button';
import { useMutation } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { toast } from 'sonner';
-import { LoginRequireButton } from '../login-require-button';
export const ApplyJoinButton = ({ onSuccess }: { onSuccess: () => void }) => {
const { groupId } = useParams<{ groupId: string }>();
diff --git a/src/components/atoms/cancel-group-button/index.tsx b/src/features/group/components/cancel-group-button.tsx
similarity index 100%
rename from src/components/atoms/cancel-group-button/index.tsx
rename to src/features/group/components/cancel-group-button.tsx
diff --git a/src/components/atoms/cancel-join-button/index.tsx b/src/features/group/components/cancel-join-button.tsx
similarity index 100%
rename from src/components/atoms/cancel-join-button/index.tsx
rename to src/features/group/components/cancel-join-button.tsx
diff --git a/src/components/molecules/gorup-action-buttons/index.tsx b/src/features/group/components/group-action-buttons.tsx
similarity index 86%
rename from src/components/molecules/gorup-action-buttons/index.tsx
rename to src/features/group/components/group-action-buttons.tsx
index bb0d44ad..527fc2bd 100644
--- a/src/components/molecules/gorup-action-buttons/index.tsx
+++ b/src/features/group/components/group-action-buttons.tsx
@@ -1,10 +1,10 @@
'use client';
import { invalidateTag } from '@/actions/invalidate';
-import { ApplyJoinButton } from '@/components/atoms/apply-join-button.tsx';
-import { CancelGroupButton } from '@/components/atoms/cancel-group-button';
-import { CancelJoinButton } from '@/components/atoms/cancel-join-button';
import { ShareButton } from '@/components/atoms/share-button';
+import { ApplyJoinButton } from '@/features/group/components/apply-join-button';
+import { CancelGroupButton } from '@/features/group/components/cancel-group-button';
+import { CancelJoinButton } from '@/features/group/components/cancel-join-button';
import useAuthStore from '@/stores/useAuthStore';
import { useState } from 'react';
diff --git a/src/components/atoms/group-description/index.tsx b/src/features/group/components/group-description.tsx
similarity index 100%
rename from src/components/atoms/group-description/index.tsx
rename to src/features/group/components/group-description.tsx
diff --git a/src/components/organisms/group-detail-card/index.tsx b/src/features/group/components/group-detail-card.tsx
similarity index 95%
rename from src/components/organisms/group-detail-card/index.tsx
rename to src/features/group/components/group-detail-card.tsx
index 55314ac7..885d1b7c 100644
--- a/src/components/organisms/group-detail-card/index.tsx
+++ b/src/features/group/components/group-detail-card.tsx
@@ -3,8 +3,8 @@ import { Badge } from '@/components/atoms/badge';
import { GroupProgress } from '@/components/atoms/group/particiapant-progress';
import { PositionBadge } from '@/components/molecules/position-badge';
import { SkillBadge } from '@/components/molecules/skill-badge';
-import { ParticipantListModal } from '@/components/organisms/participant-list-modal';
import { BookmarkButtonContainer } from '@/features/bookmark/components/bookmark-button-container';
+import { ParticipantListModal } from '@/features/group/components/participant-list-modal';
import { GroupDetail, GroupTypeName } from '@/types';
import { Position, Skill } from '@/types/enums';
import { formatYearMonthDayWithDot } from '@/utils/dateUtils';
@@ -102,7 +102,7 @@ export const GroupDetailCard = ({
{info.group.participants
- .slice(0, 3)
+ .slice(0, 5)
.map(({ userId, profileImage, email, nickname }, index) => (
0 ? '-ml-3' : ''
}`}
/>
@@ -128,7 +128,7 @@ export const GroupDetailCard = ({
};
const getZIndexClass = (index: number) => {
- const zIndexMap = ['z-10', 'z-20', 'z-30'];
+ const zIndexMap = ['z-10', 'z-20', 'z-30', 'z-40', 'z-50'];
return zIndexMap[index] ?? 'z-0';
};
diff --git a/src/components/organisms/participant-list-modal/index.tsx b/src/features/group/components/participant-list-modal.tsx
similarity index 100%
rename from src/components/organisms/participant-list-modal/index.tsx
rename to src/features/group/components/participant-list-modal.tsx
diff --git a/src/components/atoms/reply/add-rereply-button.tsx b/src/features/reply/components/add-rereply-button.tsx
similarity index 100%
rename from src/components/atoms/reply/add-rereply-button.tsx
rename to src/features/reply/components/add-rereply-button.tsx
diff --git a/src/components/molecules/reply/reply-content.tsx b/src/features/reply/components/reply-content.tsx
similarity index 97%
rename from src/components/molecules/reply/reply-content.tsx
rename to src/features/reply/components/reply-content.tsx
index 4add6bb3..b6d77a6c 100644
--- a/src/components/molecules/reply/reply-content.tsx
+++ b/src/features/reply/components/reply-content.tsx
@@ -1,7 +1,6 @@
'use client';
import { request } from '@/api/request';
-import { ReplyMeta } from '@/components/molecules/reply/reply-meta';
import { Button } from '@/components/ui/button';
import useAuthStore from '@/stores/useAuthStore';
import { Reply } from '@/types';
@@ -9,6 +8,7 @@ import { useMutation } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'sonner';
+import { ReplyMeta } from './reply-meta';
type ReplyContentProps = Reply & { parentId?: number; onDelete?: () => void };
@@ -110,13 +110,13 @@ export const ReplyContent = ({
className="max-h-20 w-full border-2 border-slate-800 rounded-sm p-3 resize-none"
/>
) : (
-
{isLocallyDeleted ? '삭제된 댓글입니다.' : content}
-
+
)}
);
diff --git a/src/components/molecules/reply/reply-form.tsx b/src/features/reply/components/reply-form.tsx
similarity index 100%
rename from src/components/molecules/reply/reply-form.tsx
rename to src/features/reply/components/reply-form.tsx
diff --git a/src/components/organisms/reply/reply-item.tsx b/src/features/reply/components/reply-item.tsx
similarity index 58%
rename from src/components/organisms/reply/reply-item.tsx
rename to src/features/reply/components/reply-item.tsx
index 18903e71..47bedac6 100644
--- a/src/components/organisms/reply/reply-item.tsx
+++ b/src/features/reply/components/reply-item.tsx
@@ -1,6 +1,6 @@
-import { ReplyContent } from '@/components/molecules/reply/reply-content';
import { Reply } from '@/types';
-import { ReplyThread } from './reply-thread';
+import { ReplyContent } from './reply-content';
+import { RereplySection } from './rereply-section';
export const ReplyItem = ({
content,
@@ -10,7 +10,7 @@ export const ReplyItem = ({
deleted,
}: Reply) => {
return (
-
+
+
+
);
};
diff --git a/src/components/organisms/reply/reply-list.tsx b/src/features/reply/components/reply-list.tsx
similarity index 76%
rename from src/components/organisms/reply/reply-list.tsx
rename to src/features/reply/components/reply-list.tsx
index 44bacfb8..67530b97 100644
--- a/src/components/organisms/reply/reply-list.tsx
+++ b/src/features/reply/components/reply-list.tsx
@@ -1,11 +1,12 @@
'use client';
-import { ReplyItem } from '@/components/organisms/reply/reply-item';
+import { useReplyScrollIntoView } from '@/features/reply/hooks/useReplyScrollIntoView';
+import { useTargetReplyParams } from '@/features/reply/hooks/useTargetReplyParams ';
import { useFetchInView } from '@/hooks/useFetchInView';
import { useFetchItems } from '@/hooks/useFetchItems';
-import { useReplyScrollIntoView } from '@/hooks/useReplyScrollIntoView';
-import { useTargetReplyParams } from '@/hooks/useTargetReplyParams ';
+import { ReplyItem } from './reply-item';
+import { Loading } from '@/components/organisms/loading';
import { useTargetReplyStore } from '@/stores/useTargetReply';
import { Reply } from '@/types';
import flattenPages from '@/utils/flattenPages';
@@ -53,8 +54,24 @@ export const ReplyList = () => {
const replies = flattenPages(data.pages);
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (replies.length === 0) {
+ return (
+
+ 아직 댓글이 없습니다.
+
+ );
+ }
+
return (
-
+
{replies.map((reply) => (
- {
{hasNextPage && !isFetchingNextPage && (
)}
-
+
);
};
diff --git a/src/components/molecules/reply/reply-meta.tsx b/src/features/reply/components/reply-meta.tsx
similarity index 100%
rename from src/components/molecules/reply/reply-meta.tsx
rename to src/features/reply/components/reply-meta.tsx
diff --git a/src/features/reply/components/reply-section.tsx b/src/features/reply/components/reply-section.tsx
new file mode 100644
index 00000000..7111830e
--- /dev/null
+++ b/src/features/reply/components/reply-section.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { ErrorBoundary } from '@/components/error-boundary';
+import { handleError } from '@/components/error-boundary/error-handler';
+import { ReplyForm } from './reply-form';
+import { ReplyList } from './reply-list';
+
+export const ReplySection = () => {
+ return (
+
+
+ 댓글
+
+
+ handleError({
+ error,
+ resetErrorBoundary,
+ defaultMessage: '댓글을 불러오는 중 문제가 발생했습니다',
+ })
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/src/components/molecules/reply/rereply-form-toggle.tsx b/src/features/reply/components/rereply-form-toggle.tsx
similarity index 90%
rename from src/components/molecules/reply/rereply-form-toggle.tsx
rename to src/features/reply/components/rereply-form-toggle.tsx
index c02372ad..e61cab4a 100644
--- a/src/components/molecules/reply/rereply-form-toggle.tsx
+++ b/src/features/reply/components/rereply-form-toggle.tsx
@@ -1,7 +1,7 @@
'use client';
-import { AddRereplyButton } from '@/components/atoms/reply/add-rereply-button';
import { useState } from 'react';
+import { AddRereplyButton } from './add-rereply-button';
import { ReplyForm } from './reply-form';
type RereplyFormToggleProps = {
diff --git a/src/components/organisms/reply/rereply-item.tsx b/src/features/reply/components/rereply-item.tsx
similarity index 88%
rename from src/components/organisms/reply/rereply-item.tsx
rename to src/features/reply/components/rereply-item.tsx
index ff71e585..b42ba913 100644
--- a/src/components/organisms/reply/rereply-item.tsx
+++ b/src/features/reply/components/rereply-item.tsx
@@ -1,6 +1,6 @@
-import { ReplyContent } from '@/components/molecules/reply/reply-content';
import { Reply } from '@/types';
import { forwardRef, useState } from 'react';
+import { ReplyContent } from './reply-content';
/** 대댓글 삭제를 위해 ReplyContent를 둘러싸기 위해 만든 컴포넌트 */
export const RereplyItem = forwardRef((props, ref) => {
diff --git a/src/components/organisms/reply/rereply-list.tsx b/src/features/reply/components/rereply-list.tsx
similarity index 94%
rename from src/components/organisms/reply/rereply-list.tsx
rename to src/features/reply/components/rereply-list.tsx
index d1045042..ff885f63 100644
--- a/src/components/organisms/reply/rereply-list.tsx
+++ b/src/features/reply/components/rereply-list.tsx
@@ -1,8 +1,8 @@
'use client';
+import { useReplyScrollIntoView } from '@/features/reply/hooks/useReplyScrollIntoView';
import { useFetchInView } from '@/hooks/useFetchInView';
import { useFetchItems } from '@/hooks/useFetchItems';
-import { useReplyScrollIntoView } from '@/hooks/useReplyScrollIntoView';
import { Reply } from '@/types';
import flattenPages from '@/utils/flattenPages';
import { useParams } from 'next/navigation';
diff --git a/src/components/organisms/reply/reply-thread.tsx b/src/features/reply/components/rereply-section.tsx
similarity index 81%
rename from src/components/organisms/reply/reply-thread.tsx
rename to src/features/reply/components/rereply-section.tsx
index 0a2197ed..675dbe9d 100644
--- a/src/components/organisms/reply/reply-thread.tsx
+++ b/src/features/reply/components/rereply-section.tsx
@@ -1,12 +1,16 @@
'use client';
-import { RereplyFormToggle } from '@/components/molecules/reply/rereply-form-toggle';
-import { RereplyList } from '@/components/organisms/reply/rereply-list';
-import { useTargetReplyParams } from '@/hooks/useTargetReplyParams ';
+import { useTargetReplyParams } from '@/features/reply/hooks/useTargetReplyParams ';
import { useTargetReplyStore } from '@/stores/useTargetReply';
import { useEffect, useState } from 'react';
+import { RereplyFormToggle } from './rereply-form-toggle';
+import { RereplyList } from './rereply-list';
-export const ReplyThread = ({ parentReplyId }: { parentReplyId: number }) => {
+export const RereplySection = ({
+ parentReplyId,
+}: {
+ parentReplyId: number;
+}) => {
const [isOpen, setIsOpen] = useState(false);
const { notificationTargetReplyId, notificationTargetRereplyId } =
useTargetReplyParams();
@@ -29,7 +33,7 @@ export const ReplyThread = ({ parentReplyId }: { parentReplyId: number }) => {
};
return (
-
+
대댓글
@@ -47,6 +51,6 @@ export const ReplyThread = ({ parentReplyId }: { parentReplyId: number }) => {
openRereplyList={toggleRereplyListHandler}
isOpenRereplyList={isOpen}
/>
-
+
);
};
diff --git a/src/hooks/useReplyScrollIntoView.ts b/src/features/reply/hooks/useReplyScrollIntoView.ts
similarity index 55%
rename from src/hooks/useReplyScrollIntoView.ts
rename to src/features/reply/hooks/useReplyScrollIntoView.ts
index 7290e4e2..d804106e 100644
--- a/src/hooks/useReplyScrollIntoView.ts
+++ b/src/features/reply/hooks/useReplyScrollIntoView.ts
@@ -37,55 +37,60 @@ export const useReplyScrollIntoView = ({
const hasSetToastRef = useRef
(false);
- useEffect(() => {
- let target: number | undefined;
+ const getTargetId = (): number | undefined => {
+ if (replyType === 'reply') return targetReplyId ?? undefined;
+ if (replyType === 'rereply') return targetRereplyId ?? undefined;
+ };
+ const clearTargetQueryAndState = () => {
+ const params = new URLSearchParams(searchParams.toString());
if (replyType === 'reply') {
- if (!targetReplyId) return;
- target = targetReplyId;
+ params.delete('replyId');
+ setTargetReply({ targetReplyId: null });
+ } else {
+ params.delete('rereplyId');
+ setTargetReply({ targetRereplyId: null });
}
+ const query = params.toString();
+ const newUrl = query ? `${pathname}?${query}` : pathname;
+ window.history.replaceState(null, '', newUrl);
+ };
+
+ const scrollToElement = (element: HTMLElement | null) => {
+ element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ };
+
+ const showReplyNotFoundToast = () => {
+ const params = new URLSearchParams(searchParams.toString());
- if (replyType === 'rereply') {
- if (!targetRereplyId) return;
- target = targetRereplyId;
+ if (params.has('replyId')) {
+ toast.error('존재하지 않는 댓글입니다.');
+ } else if (params.has('rereplyId')) {
+ toast.error('삭제된 대댓글입니다.');
}
- if (!target) return;
+ clearTargetQueryAndState();
+ setTargetReply({ targetReplyId: null, targetRereplyId: null });
+ };
+
+ useEffect(() => {
+ const targetId = getTargetId();
+ if (!targetId) return;
- const targetElement = itemRefs.current[target];
+ const targetElement = itemRefs.current[targetId];
if (targetElement) {
- targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
-
- if (replyType === 'reply') {
- setTargetReply({ targetReplyId: null });
- const newParams = new URLSearchParams(searchParams.toString());
- newParams.delete('replyId');
- const newQuery = newParams.toString();
- const newUrl = newQuery ? `${pathname}?${newQuery}` : pathname;
- window.history.replaceState(null, '', newUrl);
- } else {
- setTargetReply({ targetRereplyId: null });
- window.history.replaceState(null, '', pathname);
- }
+ scrollToElement(targetElement);
+ clearTargetQueryAndState();
} else {
- bottomRef.current?.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- });
+ scrollToElement(bottomRef.current);
+
if (!hasNextPage && !hasSetToastRef.current) {
hasSetToastRef.current = true;
- setTimeout(() => {
- const params = new URLSearchParams(searchParams.toString());
- if (params.has('replyId')) {
- toast.error('존재하지 않는 댓글입니다.');
- } else if (params.has('rereplyId')) {
- toast.error('삭제된 대댓글입니다.');
- }
- window.history.replaceState(null, '', pathname);
- }, 1000);
+ setTimeout(showReplyNotFoundToast, 1000);
}
}
+
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetReplyId, targetRereplyId, data]);
diff --git a/src/hooks/useTargetReplyParams .ts b/src/features/reply/hooks/useTargetReplyParams .ts
similarity index 100%
rename from src/hooks/useTargetReplyParams .ts
rename to src/features/reply/hooks/useTargetReplyParams .ts