Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 글 수정하기 API 구현 #124

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 50 additions & 7 deletions src/components/common/banner/ToolListBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import * as S from './ToolListBanner.styled';
import Chip from '../chip/Chip';

import { fetchCategories, fetchToolsByCategory } from '../../../apis/toolBanner/ToolBannerApi';
import { ToolSelectState, ToolProp, Category } from '../../../types/toolListBanner/ToolListBannerTypes';
import { ToolSelectState, ToolProp, Category, OriginToolType } from '../../../types/toolListBanner/ToolListBannerTypes';
import { clearSelectedTool } from '../../../utils/toolListBanner/ToolListBannerUtils';

const ToolListBanner = ({ forCommunity = false, onToolSelect = () => {} }: ToolProp) => {
const ToolListBanner = ({ originTool, forCommunity = false, onToolSelect = () => {} }: ToolProp) => {
const [toolState, setToolState] = useState<ToolSelectState>(INITIAL_TOOL_STATE);
const [categories, setCategories] = useState<Category[]>([]);
const [initialTool, setInitialTool] = useState<OriginToolType>({
toolId: originTool?.toolId ?? null,
toolName: originTool?.toolName ?? null,
toolLogo: originTool?.toolLogo ?? null,
});

// 툴 카테고리(아코디언) 조회
useEffect(() => {
const getCategories = async () => {
try {
Expand All @@ -30,33 +36,36 @@ const ToolListBanner = ({ forCommunity = false, onToolSelect = () => {} }: ToolP
getCategories();
}, []);

// 툴 카테고리(아코디언)을 클릭했을 때 해당 카테고리의 툴 조회
const handleCategoryClick = async (category: string) => {
setToolState((prev) => ({
...prev,
selectedCategory: prev.selectedCategory === category ? null : category,
}));

// 선택한 카테고리가 자유가 아니고 새로운 카테고리일 때
if (category !== '자유' && category !== toolState.selectedCategory) {
try {
const response = await fetchToolsByCategory(category);
const response = await fetchToolsByCategory(category); // 새로운 툴 목록 조회 요청
setToolState((prev) => ({
...prev,
tools: response.data.tools || [],
tools: response.data.tools || [], // 보여지는 툴 목록들 바꾸기
}));
} catch (error) {
console.error('툴 목록을 불러오는 데 실패했습니다:', error);
}
}
};

// 자유 카테고리 클릭 이벤트 처리
const handleFreeCheck = (event: React.ChangeEvent<HTMLInputElement>) => {
const isChecked = event.target.checked;
const isChecked = event.target.checked; // 자유의 체크 여부

setToolState((prev) => ({
...prev,
isFree: isChecked,
selectedTool: null,
selectedCategory: isChecked ? '자유' : null,
selectedCategory: isChecked ? '자유' : null, // 자유를 클릭했다면 자유 보여주고, 자유 체크를 해제했다면 모두 리셋
}));

onToolSelect(null);
Expand All @@ -67,15 +76,18 @@ const ToolListBanner = ({ forCommunity = false, onToolSelect = () => {} }: ToolP
<S.TitleBox>
<S.Title isSelected={!!toolState.selectedTool}>툴 선택</S.Title>
<S.Subtitle>
{/* tool이 선택되어있을 때 */}
{toolState.selectedTool || toolState.isFree ? (
<Chip size="medium" stroke>
<Chip.RectContainer>
{toolState.isFree ? (
// tool이 자유일 때
<>
<Img />
<Chip.Label>자유</Chip.Label>
</>
) : (
// tool이 자유가 아닐 때
(() => {
const selectedToolData = toolState.tools.find((tool) => tool.toolId === toolState.selectedTool);
return selectedToolData ? (
Expand All @@ -86,12 +98,43 @@ const ToolListBanner = ({ forCommunity = false, onToolSelect = () => {} }: ToolP
) : null;
})()
)}
<S.CloseBtn as="button" onClick={() => clearSelectedTool(setToolState, onToolSelect)}>
<S.CloseBtn
as="button"
onClick={() => {
clearSelectedTool(setToolState, onToolSelect);
setInitialTool({
toolId: null,
toolName: null,
toolLogo: null,
});
}}
>
<Chip.CloseIcon />
</S.CloseBtn>
</Chip.RectContainer>
</Chip>
) : initialTool.toolLogo && initialTool.toolName ? (
// 선택한 tool이 없지만 originTool이 있을 때 (게시글 수정 페이지 초기 상태)
<Chip size="medium" stroke>
<Chip.RectContainer>
<Chip.Icon src={initialTool.toolLogo as string} alt="logo" width={2} height={2} />
<Chip.Label>{initialTool.toolName}</Chip.Label>
<S.CloseBtn
as="button"
onClick={() =>
setInitialTool({
toolId: null,
toolName: null,
toolLogo: null,
})
}
>
<Chip.CloseIcon />
</S.CloseBtn>
</Chip.RectContainer>
</Chip>
) : (
// tool 선택 안했을 때
'글과 관련된 툴을 선택해주세요.'
)}
</S.Subtitle>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/postCard/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const Card = forwardRef<HTMLLIElement, CardDataProp>((props, ref) => {
</S.BottomBarLeft>
<DropDown position="end">
<DropDown.Content $display="top">
<DropDown.Item onClick={() => navigate('/community/modify/:id', { state: post })}>
<DropDown.Item onClick={() => navigate(`/community/modify/${boardId}`, { state: { post } })}>
수정하기
</DropDown.Item>
<DropDown.Item status="danger" onClick={handleModalOpen}>
Expand Down
13 changes: 13 additions & 0 deletions src/pages/communityModify/CommunityModify.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryFn } from '@storybook/react';

import CommunityModify from './CommunityModify';

export default {
title: 'Pages/CommunityModify',
component: CommunityModify,
} as Meta;

const Template: StoryFn = () => <CommunityModify />;

export const Default = Template.bind({});
Default.args = {};
48 changes: 48 additions & 0 deletions src/pages/communityModify/CommunityModify.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled from '@emotion/styled';

export const WriteWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 7.5rem;
`;

export const WriteContainer = styled.div`
display: flex;
`;

export const WriteBox = styled.div`
position: relative;
display: flex;
flex-direction: column;
gap: 1.2rem;
margin-right: 2rem;
`;

export const WriteTitle = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 104.8rem;
height: 6.4rem;
margin: 0.8rem 0;

${({ theme }) => theme.fonts.body_24_b};
color: ${({ theme }) => theme.colors.black};

cursor: pointer;
`;

export const SideBanner = styled.div`
display: flex;
flex-direction: column;
gap: 1.6rem;
`;

export const ToastBox = styled.div`
position: absolute;
top: 74rem;
left: 38rem;
z-index: 3;
`;
131 changes: 131 additions & 0 deletions src/pages/communityModify/CommunityModify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import ToolListBanner from '@components/banner/ToolListBanner';
import CircleButton from '@components/button/circleButton/CircleButton';
import Toast from '@components/toast/Toast';
import { useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import { useBoardUpdate } from './apis/queries';
import * as S from './CommunityModify.styled';
import WritingBody from './components/writingBody/WritingBody';
import WritingImg from './components/writingImg/WritingImg';
import WritingTitle from './components/writingTitle/WritingTitle';
import useCommunityModify from './hooks/UseCommunityModify';
import { PostType } from './types/PostType';
import { createPostFormData } from './utils/FormDataUtils';

const CommunityModify = () => {
const [post, setPost] = useState<PostType | null>(null);
const location = useLocation();
const navigate = useNavigate();

useEffect(() => {
if (location.state?.post) {
setPost(location.state.post);
} else {
navigate('/');
}
}, []);

const originTool = useMemo(() => {
return post
? { toolId: post.toolId, toolName: post.toolName, toolLogo: post.toolLogo }
: { toolId: 0, toolName: '알 수 없음', toolLogo: '' }; // 기본값 설정
}, [post]);

const { title, setTitle, body, setBody, images, setImages, selectedTool, isFree, handleToolSelect } =
useCommunityModify(post?.toolId as number);

const [isToastVisible, setIsToastVisible] = useState(false);
const [imageFiles, setImageFiles] = useState<File[]>([]);
const [isButtonDisabled, setIsButtonDisabled] = useState(true);
const [isImgSame, setIsImgSame] = useState(true);
const { mutate: patchMutate } = useBoardUpdate();

const handlePostSubmit = async () => {
if (isButtonDisabled || !post) return;

const formData = createPostFormData(title, body, isFree, selectedTool, images);

const req = { id: post.boardId, data: formData };
await patchMutate(req);
setIsToastVisible(true);
setTimeout(() => setIsToastVisible(false), 3000);
};

useEffect(() => {
if (post) {
setTitle(post.title);
setBody(post.content);
handleToolSelect(post.toolId);
}
}, [post]);

// 이미지 URL → File 변환
const fetchFiles = async (imageUrls: string[]): Promise<File[]> => {
const filePromises = imageUrls.map(async (imageUrl, index) => {
const response = await fetch(imageUrl);
const blob = await response.blob();
return new File([blob], `image-${index}.jpg`, { type: blob.type });
});

return await Promise.all(filePromises);
};

// post.images를 File[]로 변환하여 상태 저장
useEffect(() => {
if (post?.images) {
fetchFiles(post.images).then(setImageFiles);
}
}, []);

useEffect(() => {
if (!post) return;
const isNull = title.trim() === '' || body.trim() === '';
const isSame = title === post?.title && body === post.content && isImgSame && selectedTool === post.toolId;

console.log('isNull:', isNull);
console.log('isSame:', isSame);
console.log('isImgSame:', isImgSame);
console.log('selectedTool:', selectedTool, 'post.toolId:', post?.toolId);
console.log(isButtonDisabled);

setIsButtonDisabled(isNull || isSame);
}, [title, body, selectedTool, isImgSame]);

const handleImageUpload = (newImages: File[]) => {
setImages(newImages);
setIsImgSame(false); // 이미지 변경 플래그
};

if (!post) {
return <div>로딩 중...</div>;
}

return (
<S.WriteWrapper>
<S.WriteTitle>글 수정하기</S.WriteTitle>
<S.WriteContainer>
<S.WriteBox>
<WritingTitle originTitle={post.title} setTitle={setTitle} />
<WritingBody originBody={post.content} setBody={setBody} />
<WritingImg originImages={imageFiles} onImageUpload={handleImageUpload} />
</S.WriteBox>
<S.SideBanner>
<ToolListBanner originTool={originTool} onToolSelect={handleToolSelect} />
<CircleButton onClick={handlePostSubmit} size="large" disabled={isButtonDisabled}>
글 게시하기
</CircleButton>
</S.SideBanner>
</S.WriteContainer>
{isToastVisible && (
<S.ToastBox>
<Toast isVisible={true} isWarning={true}>
글 수정이 완료 되었습니다.
</Toast>
</S.ToastBox>
)}
</S.WriteWrapper>
);
};

export default CommunityModify;
20 changes: 20 additions & 0 deletions src/pages/communityModify/apis/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { patch } from '@apis/index';

import { PostBoardResponse } from '../types/PostType';

const postBoard = async (req: { id: number | null; data: FormData }): Promise<PostBoardResponse> => {
console.log(req.data);
try {
const response = await patch<PostBoardResponse>(`/boards/${req.id}`, req.data, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response;
} catch (error) {
console.error('수정 실패:', error);
throw new Error('수정 실패');
}
};

export default postBoard;
20 changes: 20 additions & 0 deletions src/pages/communityModify/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';

import postBoard from './api';

export const useBoardUpdate = () => {
// const userItem = localStorage.getItem('user');
// const userData = userItem ? JSON.parse(userItem) : null;
// const userId = userData?.accessToken || null;

// const queryClient = useQueryClient();
const navigate = useNavigate();

return useMutation({
mutationFn: (req: { id: number | null; data: FormData }) => postBoard(req),
onSuccess: () => {
navigate('/community');
},
});
};
Loading
Loading