Skip to content

Commit df0e254

Browse files
Merge pull request #52 from ShipFriend0516/refactor/code-quality
refactor/code-quality
2 parents 1aa7859 + 5bdcf54 commit df0e254

7 files changed

Lines changed: 327 additions & 196 deletions

File tree

.eslintrc.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"rules": {
44
"no-unused-vars": "off",
55
"@typescript-eslint/no-unused-vars": "warn",
6-
"@typescript-eslint/no-explicit-any": "warn",
7-
6+
"@typescript-eslint/no-explicit-any": "warn"
87
}
98
}
Lines changed: 43 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,53 @@
11
'use client';
22
import '@uiw/react-md-editor/markdown-editor.css';
33
import '@uiw/react-markdown-preview/markdown.css';
4-
import { useEffect, useState } from 'react';
4+
import { useState } from 'react';
55
import dynamic from 'next/dynamic';
6-
import { PostBody } from '@/app/types/Post';
7-
import { StaticImport } from 'next/dist/shared/lib/get-img-props';
8-
import axios from 'axios';
9-
import useToast from '@/app/hooks/useToast';
106
import { useBlockNavigate } from '@/app/hooks/common/useBlockNavigate';
11-
import { useRouter, useSearchParams } from 'next/navigation';
7+
import { useSearchParams } from 'next/navigation';
128
import PostWriteButtons from '@/app/entities/post/write/PostWriteButtons';
13-
import { validatePost } from '@/app/lib/utils/validate/validate';
14-
import { Series } from '@/app/types/Series';
159
import Overlay from '@/app/entities/common/Overlay/Overlay';
1610
import CreateSeriesOverlayContainer from '@/app/entities/series/CreateSeriesOverlayContainer';
17-
import { getAllSeriesData } from '@/app/entities/series/api/series';
1811
import UploadImageContainer from '@/app/entities/post/write/UploadImageContainer';
19-
import useDraft from '@/app/hooks/post/useDraft';
2012
import PostMetadataForm from '@/app/entities/post/write/PostMetadataForm';
13+
import usePost from '@/app/hooks/post/usePost';
2114

2215
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
2316

2417
const BlogForm = () => {
2518
const params = useSearchParams();
2619
const slug = params.get('slug');
27-
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
28-
const [title, setTitle] = useState('');
29-
const [subTitle, setSubTitle] = useState('');
30-
const [content, setContent] = useState<string | undefined>('');
31-
const [profileImage, setProfileImage] = useState<string | StaticImport>();
32-
const [thumbnailImage, setThumbnailImage] = useState<string | StaticImport>();
33-
const [seriesList, setSeriesList] = useState<Series[]>([]);
34-
const [seriesId, setSeriesId] = useState<string>();
35-
const [seriesLoading, setSeriesLoading] = useState(true);
36-
const [errors, setErrors] = useState<string[]>([]);
37-
const [tags, setTags] = useState<string[]>([]);
38-
const [isPrivate, setIsPrivate] = useState<boolean>(false);
3920

40-
const toast = useToast();
41-
const router = useRouter();
42-
const NICKNAME = '개발자 서정우';
43-
const [createSeriesOpen, setCreateSeriesOpen] = useState(false);
44-
// 임시저장 상태
45-
const { draft, draftImages, updateDraft, clearDraft } = useDraft();
46-
// 이미지 상태
47-
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
48-
49-
const postBody: PostBody = {
21+
const {
5022
title,
5123
subTitle,
52-
author: NICKNAME,
53-
content: content || '',
54-
profileImage,
55-
thumbnailImage,
56-
seriesId: seriesId || '',
57-
tags: tags,
58-
isPrivate: isPrivate,
59-
};
24+
submitLoading,
25+
seriesLoading,
26+
seriesId,
27+
seriesList,
28+
content,
29+
setTitle,
30+
setSubTitle,
31+
setContent,
32+
setSeriesId,
33+
setIsPrivate,
34+
isPrivate,
35+
tags,
36+
setTags,
37+
uploadedImages,
38+
setUploadedImages,
39+
overwriteDraft,
40+
saveToDraft,
41+
clearDraftInStore,
42+
submitHandler,
43+
postBody,
44+
errors,
45+
handleLinkCopy,
46+
} = usePost(slug || '');
6047

48+
const [createSeriesOpen, setCreateSeriesOpen] = useState(false);
6149
useBlockNavigate({ title, content: content || '' });
6250

63-
useEffect(() => {
64-
getSeries();
65-
}, []);
66-
67-
useEffect(() => {
68-
if (slug) {
69-
getPostDetail();
70-
}
71-
}, [slug]);
72-
73-
// 시리즈
74-
const getSeries = async () => {
75-
try {
76-
const data = await getAllSeriesData();
77-
setSeriesList(data);
78-
setSeriesId(data[0]._id);
79-
setSeriesLoading(false);
80-
} catch (e) {
81-
console.error('시리즈 조회 중 오류 발생', e);
82-
}
83-
};
84-
85-
// 블로그
86-
const postBlog = async (post: PostBody) => {
87-
try {
88-
const response = await axios.post('/api/posts', post);
89-
if (response.status === 201) {
90-
toast.success('글이 성공적으로 발행되었습니다.');
91-
router.push('/posts');
92-
}
93-
} catch (e) {
94-
toast.error('글 발행 중 오류 발생했습니다.');
95-
console.error('글 발행 중 오류 발생', e);
96-
}
97-
};
98-
99-
const updatePost = async (post: PostBody) => {
100-
try {
101-
const response = await axios.put(`/api/posts/${slug}`, post);
102-
if (response.status === 200) {
103-
toast.success('글이 성공적으로 수정되었습니다.');
104-
router.push('/posts');
105-
}
106-
} catch (e) {
107-
toast.error('글 수정 중 오류 발생했습니다.');
108-
console.error('글 수정 중 오류 발생', e);
109-
}
110-
};
111-
112-
// 임시저장 관련 함수
113-
const saveToDraft = () => {
114-
const { success } = updateDraft(postBody, uploadedImages);
115-
if (success) {
116-
toast.success('임시 저장되었습니다.');
117-
} else {
118-
toast.error('임시 저장 실패');
119-
}
120-
};
121-
122-
const overwriteDraft = () => {
123-
if (draft !== null) {
124-
if (confirm('임시 저장된 글이 있습니다. 덮어쓰시겠습니까?')) {
125-
const { title, content, subTitle, seriesId, isPrivate } = draft;
126-
setTitle(title || '');
127-
setContent(content);
128-
setSubTitle(subTitle || '');
129-
setSeriesId(seriesId);
130-
setUploadedImages(draftImages || []);
131-
setIsPrivate(isPrivate || false);
132-
}
133-
} else {
134-
toast.error('임시 저장된 글이 없습니다.');
135-
}
136-
};
137-
138-
const clearDraftInStore = () => {
139-
clearDraft();
140-
toast.success('임시 저장이 삭제되었습니다.');
141-
};
142-
143-
const submitHandler = (post: PostBody) => {
144-
try {
145-
setSubmitLoading(true);
146-
const { isValid, errors } = validatePost(post);
147-
setErrors(errors);
148-
if (!isValid) {
149-
toast.error('유효성 검사 실패');
150-
console.error('유효성 검사 실패', errors);
151-
setSubmitLoading(false);
152-
return;
153-
}
154-
155-
if (slug) {
156-
updatePost(post);
157-
} else {
158-
postBlog(post);
159-
}
160-
clearDraft();
161-
} catch (e) {
162-
console.error('글 발행 중 오류 발생', e);
163-
setSubmitLoading(false);
164-
}
165-
};
166-
167-
const getPostDetail = async () => {
168-
try {
169-
const response = await axios.get(`/api/posts/${slug}`);
170-
const data = await response.data;
171-
setTitle(data.post.title || '');
172-
setSubTitle(data.post.subTitle);
173-
setContent(data.post.content);
174-
setSeriesId(data.post.seriesId || '');
175-
setTags(data.post.tags || []);
176-
setIsPrivate(data.post.isPrivate || false);
177-
} catch (e) {
178-
console.error('글 조회 중 오류 발생', e);
179-
}
180-
};
181-
182-
const handleLinkCopy = (image: string) => {
183-
navigator.clipboard.writeText(image);
184-
toast.success('이미지 링크가 복사되었습니다.');
185-
};
186-
18751
return (
18852
<div className={'px-16'}>
18953
<PostMetadataForm
@@ -227,15 +91,7 @@ const BlogForm = () => {
22791
setUploadedImages={setUploadedImages}
22892
onClick={handleLinkCopy}
22993
/>
230-
{errors && (
231-
<div className={'mt-2'}>
232-
{errors.slice(0, 3).map((error, index) => (
233-
<p key={index} className={'text-sm text-red-500'}>
234-
{error}
235-
</p>
236-
))}
237-
</div>
238-
)}
94+
<ErrorBox errors={errors} />
23995
<PostWriteButtons
24096
slug={slug}
24197
postBody={postBody}
@@ -246,4 +102,18 @@ const BlogForm = () => {
246102
</div>
247103
);
248104
};
105+
106+
const ErrorBox = ({ errors }: { errors: string[] | null }) => {
107+
if (!errors) return null;
108+
109+
return (
110+
<div className={'mt-2'}>
111+
{errors.slice(0, 3).map((error, index) => (
112+
<p key={index} className={'text-sm text-red-500'}>
113+
{error}
114+
</p>
115+
))}
116+
</div>
117+
);
118+
};
249119
export default BlogForm;

app/entities/post/write/UploadImageContainer.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import UploadedImage from '@/app/entities/post/write/UploadedImage';
33
import { FaImage } from 'react-icons/fa';
44
import { upload } from '@vercel/blob/client';
5-
import { ChangeEvent, useState } from 'react';
5+
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
66

77
interface UploadImageContainerProps {
88
onClick: (link: string) => void;
99
uploadedImages: string[];
10-
setUploadedImages: (images: string[]) => void;
10+
setUploadedImages: Dispatch<SetStateAction<string[]>>;
1111
}
1212
const UploadImageContainer = ({
1313
onClick,
@@ -22,16 +22,28 @@ const UploadImageContainer = ({
2222
throw new Error('이미지가 선택되지 않았습니다.');
2323
}
2424

25-
const file = target.files[0];
25+
const files = target.files;
2626

27-
const timestamp = new Date().getTime();
28-
const pathname = `/images/${timestamp}-${file.name}`;
29-
const newBlob = await upload(pathname, file, {
30-
access: 'public',
31-
handleUploadUrl: '/api/upload',
32-
});
27+
if (files.length === 0) {
28+
throw new Error('업로드할 파일이 없습니다.');
29+
}
30+
31+
for (let i = 0; i < files.length; i++) {
32+
const file = files[i];
33+
if (!file.type.startsWith('image/')) {
34+
throw new Error('이미지 파일만 업로드할 수 있습니다.');
35+
}
36+
37+
const timestamp = new Date().getTime();
38+
const pathname = `/images/${timestamp}-${file.name}`;
39+
const newBlob = await upload(pathname, file, {
40+
access: 'public',
41+
handleUploadUrl: '/api/upload',
42+
});
43+
44+
setUploadedImages((prev) => [...prev, newBlob.url]);
45+
}
3346

34-
setUploadedImages([...uploadedImages, newBlob.url]);
3547
return;
3648
} catch (error) {
3749
console.error('업로드 실패:', error);

app/hooks/common/useURLSync.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useEffect, useState } from 'react';
2+
import { useRouter } from 'next/navigation';
3+
4+
interface useURLSyncConfig {
5+
baseURL: string;
6+
params: Record<string, any> | Record<string, any[]>;
7+
}
8+
9+
/**
10+
* 서치파라미터를 쉽게 설정할 수 있는 훅을 만들어봅시다.
11+
* example /posts?page=1&series=seriesSlug&query=query
12+
* 필요한 파라미터는 baseURL, currentPage, seriesSlugParam, query입니다.
13+
* @param baseURL
14+
* @param params
15+
*/
16+
17+
const useURLSync = ({ baseURL, params }: useURLSyncConfig) => {
18+
const router = useRouter();
19+
20+
useEffect(() => {
21+
const searchParams = new URLSearchParams();
22+
if (params) {
23+
Object.entries(params).forEach(([key, value]) => {
24+
if (value !== undefined && value !== null) {
25+
searchParams.set(key, String(value));
26+
}
27+
});
28+
}
29+
const finalUrl = `/${baseURL}?${searchParams.toString()}`;
30+
router.push(finalUrl);
31+
}, [...Object.values(params)]);
32+
33+
return {};
34+
};
35+
36+
export default useURLSync;

0 commit comments

Comments
 (0)