Skip to content

Commit be92a42

Browse files
authored
Merge pull request #60 from hanguswls/feature/4-save-post
글 저장 기능 구현 (#4)
2 parents 611c61b + 3bbc994 commit be92a42

File tree

11 files changed

+293
-147
lines changed

11 files changed

+293
-147
lines changed

FRONT/src/pages/CreatePost/CreatePost.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,54 @@ import Button from "../../components/Button/Button";
88
import NicknamePasswordInput from "./components/NicknamePasswordInput/NicknamePasswordInput";
99
import TagWrapper from "./components/TagWrapper/TagWrapper";
1010
import MarkdownEditor from "./components/MarkdownEditor/MarkdownEditor";
11+
import useCreatePost from "./hooks/useCreatePost";
12+
import useMarkdownEditor from "./hooks/useMarkdownEditor";
1113

1214
function CreatePost() {
15+
const { post, handlePostChange, handleSaveButtonClick } = useCreatePost();
16+
const { handleIconButtonClick, handleFileUpload, textareaRef, fileInputRef } = useMarkdownEditor({
17+
initialMarkdown: post.content,
18+
imageIds: post.imageIds,
19+
onContentChange: (value: string) => handlePostChange('content', value),
20+
onImageIdsChange: (value: number[]) => handlePostChange('imageIds', value),
21+
});
1322
const { data } = useQuery<ApiResponse<Category[]>>({
1423
queryKey: ["categories"],
1524
queryFn: () => fetchCategories(),
1625
});
1726

1827
return (
1928
<CreatePostContainer>
20-
<CategorySelector categories={data?.data as Category[]} />
21-
<PostTitleInput placeholder="제목을 입력하세요" />
22-
<MarkdownEditor />
29+
<CategorySelector
30+
categories={data?.data as Category[]}
31+
onCategorySelect={(id) => handlePostChange('categoryId', id)}
32+
/>
33+
<PostTitleInput
34+
placeholder="제목을 입력하세요"
35+
value={post.title}
36+
onChange={(e) => handlePostChange('title', e.target.value)}
37+
/>
38+
<MarkdownEditor
39+
markdown={post.content}
40+
onChange={(value) => handlePostChange('content', value)}
41+
onIconButtonClick={handleIconButtonClick}
42+
textareaRef={textareaRef}
43+
fileInputRef={fileInputRef}
44+
onFileUpload={handleFileUpload}
45+
/>
2346
<TagWrapper/>
24-
<NicknamePasswordInput />
47+
<NicknamePasswordInput
48+
author={post.author}
49+
password={post.password}
50+
onAuthorChange={(value) => handlePostChange('author', value)}
51+
onPasswordChange={(value) => handlePostChange('password', value)}
52+
/>
2553
<ButtonGroup>
2654
<Button content='임시저장' type='Primary' handleButtonClick={()=>{}} />
27-
<Button content='저장하기' type='Primary' handleButtonClick={()=>{}} />
55+
<Button content='저장' type='Primary' handleButtonClick={handleSaveButtonClick} />
2856
</ButtonGroup>
2957
</CreatePostContainer>
3058
);
3159
}
3260

33-
export default CreatePost;
61+
export default CreatePost;

FRONT/src/pages/CreatePost/components/CategorySelector/CategorySelector.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import {
1111
} from "./CategorySelector.style";
1212
import { Category } from "../../../../types/category";
1313

14-
function CategorySelector({ categories }: { categories: Category[] }) {
14+
interface CategorySelectorProps {
15+
categories: Category[],
16+
onCategorySelect: (id: number) => void,
17+
}
18+
19+
function CategorySelector({ categories, onCategorySelect}: CategorySelectorProps) {
1520
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
1621
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
1722

1823
const handleSelect = (category: Category) => {
1924
setSelectedCategory(category);
25+
if (category.id) onCategorySelect(category.id);
2026
setIsDropdownOpen(false);
2127
console.log(category.id);
2228
};

FRONT/src/pages/CreatePost/components/MarkdownEditor/MarkdownEditor.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@ import Markdown from "react-markdown";
44
import remarkGfm from "remark-gfm";
55
import CodeBlock from "./CodeBlock";
66
import ToolBar from "../ToolBar/ToolBar";
7-
import useMarkdownEditor from "./useMarkdownEditor";
87

9-
function MarkdownEditor() {
10-
const {
11-
markdown,
12-
handleMarkdownChange,
13-
handleIconButtonClick,
14-
handleFileUpload,
15-
textareaRef,
16-
fileInputRef,
17-
} = useMarkdownEditor();
18-
8+
interface MarkdownEditorProps {
9+
markdown: string,
10+
textareaRef: React.RefObject<HTMLTextAreaElement>,
11+
fileInputRef: React.RefObject<HTMLInputElement>,
12+
onChange: (value: string) => void,
13+
onIconButtonClick: (action: string) => void,
14+
onFileUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
15+
}
16+
17+
function MarkdownEditor({
18+
markdown,
19+
onChange,
20+
onIconButtonClick,
21+
textareaRef,
22+
fileInputRef,
23+
onFileUpload,
24+
}: MarkdownEditorProps) {
25+
1926
const markdownGrammar = `# 제목\n## 부제목\n*기울임* _기울임_\n**굵게** __굵게__\n[링크](https://www.google.com)\n![이미지](이미지 주소)\n> 인용문\n* 순서가 없는 목록\n- 순서가 없는 목록\n1. 순서가 있는 목록\n1) 순서가 있는 목록\n띄어쓰기 3번 하고 목록 => 하위목록\n--- 수평선 *** 수평선\n\`한 줄 코드\`\n\`\`\`\n코드블럭\n\`\`\`\n여러 줄의 공백\n&nbsp;\n&nbsp;\n`;
2027
const [viewMode, setViewMode] = useState<'write' | 'preview'>('write');
2128

@@ -24,14 +31,14 @@ function MarkdownEditor() {
2431
<ToolBar
2532
viewMode={viewMode}
2633
setViewMode={setViewMode}
27-
handleIconButtonClick={handleIconButtonClick}
34+
onIconButtonClick={onIconButtonClick}
2835
handleAddPhotoClick={() => fileInputRef.current?.click()}
2936
/>
3037
{viewMode === "write" ? (
3138
<Editor
3239
ref={textareaRef}
3340
value={markdown}
34-
onChange={(e) => handleMarkdownChange(e.target.value)}
41+
onChange={(e) => onChange(e.target.value)}
3542
placeholder={markdownGrammar}
3643
/>
3744
) : (
@@ -53,7 +60,7 @@ function MarkdownEditor() {
5360
accept="image/*"
5461
style={{ display: "none" }}
5562
ref={fileInputRef}
56-
onChange={handleFileUpload}
63+
onChange={onFileUpload}
5764
/>
5865
</Container>
5966
);

FRONT/src/pages/CreatePost/components/MarkdownEditor/useMarkdownEditor.ts

Lines changed: 0 additions & 107 deletions
This file was deleted.

FRONT/src/pages/CreatePost/components/NicknamePasswordInput/NicknamePasswordInput.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
1-
import { useState } from 'react';
21
import { InputContainer, StyledInput } from './NicknamePasswordInput.style';
32

4-
function NicknamePasswordInput() {
5-
const [nickname, setNickname] = useState('');
6-
const [password, setPassword] = useState('');
3+
interface NicknamePasswordInput {
4+
author: string,
5+
password: string,
6+
onAuthorChange: (value: string) => void;
7+
onPasswordChange: (value: string) => void;
8+
}
79

10+
function NicknamePasswordInput({author, password, onAuthorChange, onPasswordChange}: NicknamePasswordInput) {
811
return (
912
<InputContainer>
1013
<StyledInput
1114
placeholder="닉네임을 입력하세요"
12-
value={nickname}
13-
onChange={(e) => setNickname(e.target.value)}
15+
value={author}
16+
onChange={(e) => onAuthorChange(e.target.value)}
17+
required
1418
/>
1519
<StyledInput
1620
type="password"
1721
placeholder="비밀번호를 입력하세요"
1822
value={password}
19-
onChange={(e) => setPassword(e.target.value)}
23+
onChange={(e) => onPasswordChange(e.target.value)}
24+
required
2025
/>
2126
</InputContainer>
2227
);

FRONT/src/pages/CreatePost/components/ToolBar/ToolBar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,34 @@ import linkIcon from "@_assets/icons/toolbar/link.svg";
1010
type ToolBarProps = {
1111
viewMode: "write" | "preview";
1212
setViewMode: (mode: "write" | "preview") => void;
13-
handleIconButtonClick: (action: string) => void;
13+
onIconButtonClick: (action: string) => void;
1414
handleAddPhotoClick: () => void;
1515
};
1616

17-
function ToolBar({ viewMode, setViewMode, handleIconButtonClick, handleAddPhotoClick }: ToolBarProps) {
17+
function ToolBar({ viewMode, setViewMode, onIconButtonClick, handleAddPhotoClick }: ToolBarProps) {
1818
return (
1919
<ToolBarContainer>
2020
<Group>
21-
<IconButton onClick={() => handleIconButtonClick("bold")}>
21+
<IconButton onClick={() => onIconButtonClick("bold")}>
2222
<img src={boldIcon} alt="굵게" />
2323
</IconButton>
24-
<IconButton onClick={() => handleIconButtonClick("italic")}>
24+
<IconButton onClick={() => onIconButtonClick("italic")}>
2525
<img src={italicIcon} alt="기울이기" />
2626
</IconButton>
27-
<IconButton onClick={() => handleIconButtonClick("underline")}>
27+
<IconButton onClick={() => onIconButtonClick("underline")}>
2828
<img src={underlineIcon} alt="밑줄" />
2929
</IconButton>
3030
<Separator />
3131
<IconButton onClick={handleAddPhotoClick}>
3232
<img src={addPhotoIcon} alt="사진 추가" />
3333
</IconButton>
34-
<IconButton onClick={() => handleIconButtonClick("quote")}>
34+
<IconButton onClick={() => onIconButtonClick("quote")}>
3535
<img src={quoteIcon} alt="인용문 추가" />
3636
</IconButton>
37-
<IconButton onClick={() => handleIconButtonClick("code")}>
37+
<IconButton onClick={() => onIconButtonClick("code")}>
3838
<img src={codeIcon} alt="코드 추가" />
3939
</IconButton>
40-
<IconButton onClick={() => handleIconButtonClick("link")}>
40+
<IconButton onClick={() => onIconButtonClick("link")}>
4141
<img src={linkIcon} alt="링크 추가" />
4242
</IconButton>
4343
</Group>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useState } from 'react';
2+
import { useMutation } from '@tanstack/react-query';
3+
import { createPost } from '@_services/createPostApi';
4+
import { useNavigate } from 'react-router-dom';
5+
import { CreatePostParams } from '../../../types/post';
6+
7+
function useCreatePost() {
8+
const navigate = useNavigate();
9+
const [post, setPost] = useState<CreatePostParams>({
10+
title: '',
11+
content: '',
12+
author: '',
13+
password: '',
14+
categoryId: 0,
15+
hashTagIds: [],
16+
imageIds: [],
17+
});
18+
19+
const handlePostChange = (field: keyof CreatePostParams, value: string | number | number[]) => {
20+
setPost((prev) => ({ ...prev, [field]: value }));
21+
};
22+
23+
const handleMarkdownChange = (markdown: string) => {
24+
handlePostChange('content', markdown);
25+
};
26+
27+
const mutation = useMutation({
28+
mutationFn: (newPost: CreatePostParams) => createPost(newPost),
29+
onSuccess: () => navigate('/'),
30+
onError: () => alert("저장에 실패하였습니다.")
31+
});
32+
33+
const validateRequiredFields = () => {
34+
const missingFields = [];
35+
36+
if (!post['title']) missingFields.push('제목');
37+
if (!post['content']) missingFields.push('내용');
38+
if (!post['author']) missingFields.push('닉네임');
39+
if (!post['password']) missingFields.push('비밀번호');
40+
if (!post['categoryId']) missingFields.push('카테고리');
41+
42+
if (missingFields.length > 0) {
43+
alert(`${missingFields.join(' ,')} 항목을 입력해주세요`);
44+
return false;
45+
}
46+
return true
47+
}
48+
49+
const handleSaveButtonClick = () => {
50+
if (validateRequiredFields()) {
51+
mutation.mutate(post);
52+
}
53+
};
54+
55+
return {
56+
post,
57+
handlePostChange,
58+
handleMarkdownChange,
59+
handleSaveButtonClick,
60+
};
61+
};
62+
63+
export default useCreatePost;

0 commit comments

Comments
 (0)