Skip to content

Commit

Permalink
Merge pull request #187 from epigram5-9/merge/FE-29
Browse files Browse the repository at this point in the history
FE-29 🔀 브랜치 최신화
  • Loading branch information
jangmoonwon authored Aug 4, 2024
2 parents 28deda9 + 38c0370 commit 9feefae
Show file tree
Hide file tree
Showing 46 changed files with 642 additions and 416 deletions.
Empty file.
5 changes: 5 additions & 0 deletions public/icon/menu-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon/privateIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon/publicIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions public/icon/user-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/icon/x-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/noComment.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/unlikeIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/apis/epigramComment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export const getEpigramComments = async (params: CommentRequestType): Promise<Co
}
};

export const getMyEpigramComments = async (params: CommentRequestType): Promise<CommentResponseType> => {
const { id, ...restParams } = params;
const response = await httpClient.get(`/users/${id}/comments`, {
params: restParams,
});
return response.data;
};

export const postComment = async (commentData: PostCommentRequest) => {
const response = await httpClient.post('/comments', commentData);
return response.data;
Expand Down
22 changes: 19 additions & 3 deletions src/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { EpigramRequestType } from '@/schema/epigram';
import { CommentRequestType } from '@/schema/comment';
import { GetMonthlyEmotionLogsRequestType } from '@/schema/emotion';
import { GetEpigramsParamsType } from '@/schema/epigrams';
import { getMe, getUser } from './user';
import { getEpigram } from './epigram';
import { getEpigramComments } from './epigramComment';
import { getMe, getUser, getMyContentCount } from './user';
import { getEpigram, toggleEpigramLike } from './epigram';
import { getEpigramComments, getMyEpigramComments } from './epigramComment';
import getMonthlyEmotionLogs from './emotion';
import getEpigrams from './getEpigrams';

Expand All @@ -20,6 +20,10 @@ const queries = createQueryKeyStore({
queryKey: [request],
queryFn: () => getUser(request),
}),
getMyContentCount: (request: GetUserRequestType) => ({
queryKey: ['getMyContentCount', request],
queryFn: () => getMyContentCount(request),
}),
},
// NOTE: Epigram 관련 query함수
epigram: {
Expand All @@ -33,12 +37,24 @@ const queries = createQueryKeyStore({
},
enabled: request.id !== undefined,
}),
toggleLike: (request: EpigramRequestType) => ({
queryKey: ['toggleEpigramLike', request.id],
mutationFn: (variables: EpigramRequestType) => toggleEpigramLike(variables),
}),
},
epigramComment: {
getComments: (request: CommentRequestType) => ({
queryKey: ['epigramComments', request],
queryFn: () => getEpigramComments(request),
}),
getCommentList: (request: CommentRequestType) => ({
queryKey: ['epigramComments', request] as const,
queryFn: ({ pageParam }: { pageParam: number | undefined }) => getEpigramComments({ ...request, cursor: pageParam }),
}),
getMyComments: (request: CommentRequestType) => ({
queryKey: ['myEpigramComments', request],
queryFn: () => getMyEpigramComments(request),
}),
},
emotion: {
getMonthlyEmotionLogs: (request: GetMonthlyEmotionLogsRequestType) => ({
Expand Down
16 changes: 15 additions & 1 deletion src/apis/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType } from '@/schema/user';
import type { GetUserResponseType, GetUserRequestType, PatchMeRequestType, PostPresignedUrlRequestType, PostPresignedUrlResponseType, GetMyContentCountType } from '@/schema/user';
import httpClient from '.';

export const getMe = async (): Promise<GetUserResponseType> => {
Expand All @@ -23,3 +23,17 @@ export const createPresignedUrl = async (request: PostPresignedUrlRequestType):
const response = await httpClient.post('/images/upload', formData);
return response.data;
};

export const getMyContentCount = async (request: GetUserRequestType): Promise<GetMyContentCountType> => {
const { id } = request;

// 에피그램 카운트
const epigram = await httpClient.get(`/epigrams`, { params: { limit: 1, cursor: 0, writerId: id } });

// 댓글 카운트
const comment = await httpClient.get(`/users/${id}/comments`, { params: { limit: 1, cursor: 0 } });

const response = { epigramCount: epigram.data.totalCount, commentCount: comment.data.totalCount };

return response;
};
2 changes: 1 addition & 1 deletion src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function Header({ isLogo, icon, insteadOfLogo, isButton, isProfileIcon, isShareI

return (
<nav className='bg-white h-13 px-6 py-4 md:px-28 md:py-5 lg:px-30 lg:py-6'>
<div className='container flex justify-between items-center'>
<div className='container max-w-screen-xl mx-auto flex justify-between items-center'>
<div className='flex items-center space-x-4'>
{icon === 'back' && (
<button className='w-5 h-5 lg:w-9 lg:h-9' type='button' onClick={handleBack} aria-label='뒤로가기 버튼'>
Expand Down
63 changes: 63 additions & 0 deletions src/components/Header/NewHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { useQuery } from '@tanstack/react-query';
import queries from '@/apis/queries';
import LOGO_ICON from '../../../public/epigram-icon.png';
import PROFILE_ICON from '../../../public/icon/user-icon.svg';
import MENU_ICON from '../../../public/icon/menu-icon.svg';
import Sidebar from './SideBar';

export default function NewHeader() {
const router = useRouter();
const [isSidebarOpen, setIsSidebarOpen] = useState(false);

const { data, isLoading, error } = useQuery(queries.user.getMe());

const handleNavigateTo = (path: string) => {
router.push(path);
};

const getNickName = () => {
if (isLoading) {
return '로딩 중...';
}
if (error) {
return '에러 발생';
}
return data?.nickname || '김코드';
};

return (
<div className='w-full px-6 py-4 bg-white border-b border-line-100 flex items-center gap-2.5 lg:px-[120px] md:px-[72px] md:h-[60px] lg:h-20 md:py-[19px] lg:py-[26px]'>
<div className='flex justify-between items-center w-full md:w-[744px] lg:w-[1920px]'>
<div className='flex items-center gap-3 md:gap-6 lg:gap-9'>
<div className='flex items-center gap-3'>
<button type='button' onClick={() => setIsSidebarOpen(!isSidebarOpen)} className='md:hidden'>
<Image className='w-5 h-5 lg:w-9 lg:h-9' src={MENU_ICON} alt='menu' />
</button>
<button type='button' onClick={() => handleNavigateTo('/epigrams')} className='flex items-center gap-1'>
<Image className='w-6 h-6 lg:w-9 lg:h-9' src={LOGO_ICON} alt='logo' />
<span className='text-black-700 text-6 lg:text-[26px] font-montserrat leading-6 font-bold'>Epigram</span>
</button>
</div>
<div className='hidden md:flex items-center gap-6'>
<button type='button' onClick={() => handleNavigateTo('/feed')}>
<div className='text-center text-black-600 text-sm lg:text-base font-semibold font-pretendard leading-normal'>피드</div>
</button>
<button type='button' onClick={() => handleNavigateTo('/search')}>
<div className='text-center text-black-600 text-sm lg:text-base font-semibold font-pretendard leading-normal'>검색</div>
</button>
</div>
</div>
<button type='button' onClick={() => handleNavigateTo('/mypage')} className='flex items-center gap-1.5'>
<div className='w-4 h-4 lg:w-6 lg:h-6 relative'>
<Image src={PROFILE_ICON} alt='프로필 이미지' />
</div>
<div className='text-gray-300 text-[13px] lg:text-sm font-medium font-pretendard leading-snug'>{getNickName()}</div>
</button>
</div>
<Sidebar isOpen={isSidebarOpen} toggleSidebar={() => setIsSidebarOpen(false)} />
</div>
);
}
42 changes: 42 additions & 0 deletions src/components/Header/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import X_ICON from '../../../public/icon/x-icon.svg';

interface SidebarProps {
isOpen: boolean;
toggleSidebar: () => void;
}

function Sidebar({ isOpen, toggleSidebar }: SidebarProps) {
const router = useRouter();

const handleNavigateTo = (path: string) => {
router.push(path);
toggleSidebar();
};

if (!isOpen) {
return null;
}

return (
<div className='absolute top-0 left-0 h-full w-[220px] bg-white shadow-lg z-50'>
<div className='h-[54px] px-4 py-2.5 bg-white border-b border-[#f2f2f2] flex items-center justify-end'>
<button type='button' onClick={toggleSidebar}>
<Image className='w-6 h-6' src={X_ICON} alt='사이드바 닫기' />
</button>
</div>
<div className='flex flex-col'>
<button type='button' className='w-full px-5 py-6 bg-white text-left' onClick={() => handleNavigateTo('/feed')}>
<div className='text-[#373737] text-base font-medium font-pretendard leading-relaxed'>피드</div>
</button>
<button type='button' className='w-full px-5 py-6 bg-white text-left' onClick={() => handleNavigateTo('/search')}>
<div className='text-[#373737] text-base font-medium font-pretendard leading-relaxed'>검색</div>
</button>
</div>
</div>
);
}

export default Sidebar;
24 changes: 16 additions & 8 deletions src/components/epigram/Comment/CommentItem.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { CommentType } from '@/schema/comment';
import { sizeStyles, textSizeStyles, gapStyles, paddingStyles, contentWidthStyles } from '@/styles/CommentCardStyles';
import { textSizeStyles, gapStyles, paddingStyles, contentWidthStyles } from '@/styles/CommentCardStyles';
import getCustomRelativeTime from '@/lib/dateUtils';
import useDeleteCommentMutation from '@/hooks/useDeleteCommentHook';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
import DeleteAlertModal from '../DeleteAlertModal';
import CommentTextarea from './CommentTextarea';

interface CommentItemProps {
comment: CommentType;
status?: 'view' | 'edit';
onEditComment: (id: number, content: string, isPrivate: boolean) => void;
onEditComment: (id: number) => void;
isEditing: boolean;
epigramId: number;
}

function CommentItem({ comment, status, onEditComment }: CommentItemProps) {
function CommentItem({ comment, status, onEditComment, isEditing, epigramId }: CommentItemProps) {
const deleteCommentMutation = useDeleteCommentMutation();
const { toast } = useToast();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);

const handleEditClick = () => {
onEditComment(comment.id, comment.content, comment.isPrivate);
onEditComment(comment.id);
};

// NOTE: 댓글 삭제
// NOTE: 수정 중일 때(isEditing===true) CommentTextarea를 수정모드로 렌더링
if (isEditing) {
return <CommentTextarea epigramId={epigramId} editingComment={comment} onEditComplete={() => onEditComment(0)} />;
}

const handleDeleteComment = async () => {
try {
await deleteCommentMutation.mutateAsync(comment.id);
Expand All @@ -42,9 +49,9 @@ function CommentItem({ comment, status, onEditComment }: CommentItemProps) {

return (
<div
className={`bg-slate-100 border-t border-slate-300 flex-col justify-start items-start gap-2.5 inline-flex ${sizeStyles.sm} ${sizeStyles.md} ${sizeStyles.lg} ${paddingStyles.sm} ${paddingStyles.md} ${paddingStyles.lg}`}
className={`h-auto bg-slate-100 border-t border-slate-300 flex-col justify-start items-start gap-2.5 inline-flex w-[360px] md:w-[384px] lg:w-[640px] ${paddingStyles.sm} ${paddingStyles.md} ${paddingStyles.lg}`}
>
<div className='justify-start items-start gap-4 inline-flex'>
<div className='h-full justify-start items-start gap-4 inline-flex'>
<div className='w-12 h-12 relative'>
<div className='w-12 h-12 bg-zinc-300 rounded-full overflow-hidden flex items-center justify-center'>
<Image src={comment.writer.image || '/ProfileTestImage.jpg'} alt='프로필 이미지' layout='fill' objectFit='cover' className='rounded-full' />
Expand All @@ -57,6 +64,7 @@ function CommentItem({ comment, status, onEditComment }: CommentItemProps) {
<div className={`text-zinc-600 font-normal font-pretendard leading-normal ${textSizeStyles.sm.time} ${textSizeStyles.md.time} ${textSizeStyles.lg.time}`}>
{getCustomRelativeTime(comment.createdAt)}
</div>
<Image src={comment.isPrivate ? '/icon/privateIcon.png' : '/Icon/publicIcon.png'} width={16} height={16} alt={comment.isPrivate ? '비공개' : '공개'} />
</div>
{status === 'edit' && (
<div className='justify-start items-start gap-4 flex'>
Expand All @@ -78,7 +86,7 @@ function CommentItem({ comment, status, onEditComment }: CommentItemProps) {
)}
</div>
<div
className={`w-full text-zinc-800 font-normal font-pretendard ${textSizeStyles.sm.content} ${textSizeStyles.md.content} ${textSizeStyles.lg.content} ${contentWidthStyles.sm} ${contentWidthStyles.md} ${contentWidthStyles.lg}`}
className={`w-full text-zinc-800 font-normal font-pretendard whitespace-pre ${textSizeStyles.sm.content} ${textSizeStyles.md.content} ${textSizeStyles.lg.content} ${contentWidthStyles.sm} ${contentWidthStyles.md} ${contentWidthStyles.lg}`}
>
{comment.content}
</div>
Expand Down
16 changes: 11 additions & 5 deletions src/components/epigram/Comment/CommentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import NoComment from './NoComment';
import CommentItem from './CommentItem';

interface CommentListProps extends Omit<EpigramCommentProps, 'userImage'> {
onEditComment: (id: number, content: string, isPrivate: boolean) => void;
onEditComment: (id: number) => void;
editingCommentId: number | null;
}

function CommentList({ epigramId, currentUserId, onEditComment }: CommentListProps) {
function CommentList({ epigramId, currentUserId, onEditComment, editingCommentId }: CommentListProps) {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useEpigramCommentsQuery(epigramId);

const observerRef = useRef<IntersectionObserver | null>(null);
const lastCommentRef = useRef<HTMLDivElement | null>(null);

// NOTE: Observer 콜백: 마지막 요소가 화면에 보이면 다음 페이지(댓글 최대3개를 묶어 1페이지 취급) 로드
const handleObserver = useCallback(
(entries: IntersectionObserverEntry[]) => {
const [target] = entries;
Expand All @@ -39,7 +39,6 @@ function CommentList({ epigramId, currentUserId, onEditComment }: CommentListPro
}

return () => {
// NOTE: effect가 실행되기전에 호출해서 메모리 누수를 방지해줌
if (observerRef.current) {
observerRef.current.disconnect();
}
Expand All @@ -58,7 +57,14 @@ function CommentList({ epigramId, currentUserId, onEditComment }: CommentListPro
{allComments.length > 0 ? (
<>
{allComments.map((comment) => (
<CommentItem key={comment.id} comment={comment} status={comment.writer.id === currentUserId ? 'edit' : 'view'} onEditComment={onEditComment} />
<CommentItem
key={comment.id}
comment={comment}
status={comment.writer.id === currentUserId ? 'edit' : 'view'}
onEditComment={onEditComment}
isEditing={editingCommentId === comment.id}
epigramId={epigramId}
/>
))}
<div ref={lastCommentRef}>{isFetchingNextPage && <div>더 많은 댓글을 불러오는 중...</div>}</div>
</>
Expand Down
Loading

0 comments on commit 9feefae

Please sign in to comment.