Skip to content

Commit

Permalink
Merge pull request #46 from NohWookJin/Refactor/#43
Browse files Browse the repository at this point in the history
Refactor(#43): Image Upload and Preview
  • Loading branch information
NohWookJin authored Jun 6, 2024
2 parents 4b9e521 + 526e514 commit 74b6f58
Show file tree
Hide file tree
Showing 11 changed files with 1,976 additions and 236 deletions.
1,522 changes: 1,513 additions & 9 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
"preview": "vite preview"
},
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^7.0.0",
"@xeger/quill-image-actions": "^0.7.2",
"@xeger/quill-image-formats": "^0.7.2",
"axios": "^1.6.8",
"draft-js": "^0.11.7",
"framer-motion": "^11.2.6",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"quill-image-resize-module": "^3.0.0",
"react": "^18.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.2.0",
"react-draft-wysiwyg": "^1.15.0",
"react-quill": "^2.0.0",
"react-router-dom": "^6.22.3",
"react-scroll": "^1.9.0",
Expand Down
34 changes: 2 additions & 32 deletions src/API/getRoutineBlog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,37 +54,7 @@ export const getRoutineBlogDetail = async (
`/routines/${routineId}/blogs/${contentId}`
);

console.log(data);

return data;
};

// {
// "blog": {
// "id": 38,
// "title": "5월 23일 오전 2시 17분 (2)",
// "content": "5월 23일 오전 2시 17분",
// "imagePath": null,
// "date": "2024-05-22T08:18:00.545Z",
// "routine": {
// "id": 48,
// "userId": 15,
// "name": "Blog",
// "date": "2024-05-22T08:17:45.962Z",
// "routineType": "blog",
// "targetCount": 1,
// "colorSelection": "#3a7ce1",
// "isDeleted": false
// }
// },
// "today": [],
// "past": {
// "2024-05-22": [
// {
// "id": 38,
// "title": "5월 23일 오전 2시 17분 (2)",
// "content": "5월 23일 오전 2시 17분",
// "imagePath": null,
// "date": "2024-05-22T08:18:00.545Z"
// }
// ]
// }
// }
68 changes: 58 additions & 10 deletions src/API/routinesBlog.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,90 @@
import { instance } from "./../lib/userAuth";
export interface UploadImageResponse {
imageUrl: string;
}

export const postBlog = async (routineId: number, formData: FormData) => {
export interface BlogData {
title: string;
content: string;
imagePath?: string;
}

export interface BlogResponse {
id: number;
date: string;
title: string;
content: string;
imagePath: string;
}

// 이미지 업로드 함수
export const uploadImage = async (
routineId: number,
imageData: FormData
): Promise<UploadImageResponse> => {
try {
const { data } = await instance.post(
`/routines/${routineId}/blogs`,
formData,
const { data } = await instance.post<UploadImageResponse>(
`/routines/${routineId}/blogs/file`,
imageData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);

console.log(data);

return data;
} catch (error) {
console.error(error);
throw error;
}
};

// 블로그 생성 함수
export const postBlog = async (
routineId: number,
blogData: BlogData
): Promise<BlogResponse> => {
try {
const { data } = await instance.post<BlogResponse>(
`/routines/${routineId}/blogs`,
blogData,
{
headers: {
"Content-Type": "application/json",
},
}
);

return data;
} catch (error) {
console.error(error);
throw error;
}
};

export const patchBlog = async (
routineId: number,
contentId: number,
formData: FormData
) => {
blogData: BlogData
): Promise<BlogResponse> => {
try {
const { data } = await instance.patch(
const { data } = await instance.patch<BlogResponse>(
`/routines/${routineId}/blogs/${contentId}`,
formData,
blogData,
{
headers: {
"Content-Type": "multipart/form-data",
"Content-Type": "application/json",
},
}
);

return data;
} catch (error) {
console.error(error);
throw error;
}
};

Expand All @@ -45,9 +93,9 @@ export const deleteBlog = async (routineId: number, contentId: number) => {
const res = await instance.delete(
`/routines/${routineId}/blogs/${contentId}`
);

return res;
} catch (error) {
console.error(error);
throw error;
}
};
21 changes: 17 additions & 4 deletions src/Components/RotineTodoDetail/RoutineTodoDetailTodo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ const RoutineTodoDetailTodo = ({
};

return (
<div className="mb-[60px] bg-[#F4F4F8] w-full flex flex-col justify-between shadow-2xl rounded-lg p-4">
<div
className={`mb-[60px] w-full flex flex-col justify-between shadow-2xl rounded-lg p-4 ${
darkMode
? "dark: bg-[#23272f] text-white border border-opacity-30"
: " bg-[#F4F4F8]"
}`}
>
{todos.length !== 0 && (
<div className="flex flex-col gap-[15px]">
{todos
Expand All @@ -48,13 +54,20 @@ const RoutineTodoDetailTodo = ({
</div>
)}

<form onSubmit={onSubmitNewContent} className="mt-[20px]">
<form
onSubmit={onSubmitNewContent}
className={`mt-[20px] ${
darkMode ? "dark: bg-[#23272f] text-white" : ""
}`}
>
<input
type="text"
value={newTodoContent}
onChange={(e) => setNewTodoContent(e.target.value)}
className={`border-b border-[#d9d9d9] focus:outline-none w-full p-2 bg-[#F4F4F8] ${
darkMode ? "dark: text-black" : ""
className={` focus:outline-none w-full p-2 ${
darkMode
? "dark: bg-[#23272f] text-white"
: "border-b border-[#d9d9d9] bg-[#F4F4F8]"
}`}
placeholder="새로운 투두를 작성해보세요."
/>
Expand Down
18 changes: 11 additions & 7 deletions src/Components/RotineTodoDetail/RoutineTodoDetailTodoItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ const RoutineTodoDetailTodoItem = ({
}, [isEditing]);

return (
<div className="flex items-center justify-between border-b border-[#d9d9d9] pb-2">
<div
className={`flex items-center justify-between border-b border-[#d9d9d9] pb-2 ${
darkMode ? "dark: bg-[#23272f] text-white" : ""
}`}
>
<div className="flex items-center gap-2">
<input
type="checkbox"
Expand All @@ -59,16 +63,16 @@ const RoutineTodoDetailTodoItem = ({
value={editedContent}
ref={inputRef}
onChange={(e) => setEditedContent(e.target.value)}
className={`focus:outline-none w-full bg-[#F4F4F8] ${
darkMode ? "dark: text-black" : ""
className={`focus:outline-none w-full ${
darkMode ? "dark: bg-[#23272f] text-white" : "bg-[#F4F4F8]"
}`}
/>
</form>
) : (
<span
className={`truncate ${
isCompleted ? "line-through opacity-60" : ""
} ${darkMode ? "dark: text-black" : ""}`}
} ${darkMode ? "dark: text-white" : ""}`}
>
{content}
</span>
Expand All @@ -78,7 +82,7 @@ const RoutineTodoDetailTodoItem = ({
{isEditing ? (
<span
className={`text-xs opacity-60 cursor-pointer ${
darkMode ? "dark: text-black" : ""
darkMode ? "dark: text-white" : ""
}`}
onClick={onSaveEditContent}
>
Expand All @@ -88,15 +92,15 @@ const RoutineTodoDetailTodoItem = ({
<>
<span
className={`text-xs opacity-60 cursor-pointer ${
darkMode ? "dark: text-black" : ""
darkMode ? "dark: text-white" : ""
}`}
onClick={() => setIsEditing(true)}
>
수정
</span>
<span
className={`text-xs opacity-60 cursor-pointer ${
darkMode ? "dark: text-black" : ""
darkMode ? "dark: text-white" : ""
}`}
onClick={() => onDeleteTodo(id)}
>
Expand Down
5 changes: 5 additions & 0 deletions src/Components/RoutineBlogDetail/RoutineBlogDetailBlog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import RoutineBlogDetailBlogItem from "./RoutineBlogDetailBlogItem";
import type { Blog } from "../../API/getRoutineBlog";
import { themeState } from "../../Store/themeState";
import { useRecoilValue } from "recoil";

interface RoutineBlogDetailBlogProps {
blog: Blog[];
Expand All @@ -10,6 +12,8 @@ const RoutineBlogDetailBlog = ({
blog,
routineId,
}: RoutineBlogDetailBlogProps) => {
const darkMode = useRecoilValue(themeState);

return (
<div className="mb-[60px] w-full flex flex-col justify-between ">
{blog.length === 0 ? (
Expand All @@ -25,6 +29,7 @@ const RoutineBlogDetailBlog = ({
key={blog.id}
blog={blog}
routineId={routineId}
isDark={darkMode}
/>
))}
</div>
Expand Down
58 changes: 52 additions & 6 deletions src/Components/RoutineBlogDetail/RoutineBlogDetailBlogItem.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useNavigate } from "react-router-dom";
import type { Blog } from "../../API/getRoutineBlog";
import { formatDate } from "../../lib/timeFormatChange";

interface RoutineBlogDetailBlogItemProps {
blog: Blog;
routineId: number;
isDark: boolean;
}

const RoutineBlogDetailBlogItem = ({
blog,
routineId,
isDark,
}: RoutineBlogDetailBlogItemProps) => {
const navigate = useNavigate();

const formatDate = (date: Date) => {
const formatTodayDate = (date: Date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
Expand All @@ -24,23 +27,66 @@ const RoutineBlogDetailBlogItem = ({
title: string,
content: string
) => {
const date = String(formatDate(new Date()));
const date = String(formatTodayDate(new Date()));

navigate(`/routine/${routineId}/detail/${date}/${id}`, {
state: { id, date, title, content },
});
};

const extractPreviewImageUrl = (content: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const img = doc.querySelector("img");
return img ? img.src : null;
};

const removeImagesFromContent = (content: string) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const images = doc.querySelectorAll("img");
images.forEach((img) => img.remove());
return doc.body.innerHTML;
};
const previewImageUrl =
extractPreviewImageUrl(blog.content) ||
"https://via.placeholder.com/100x100?text=No+Image";
const contentWithoutImage = removeImagesFromContent(blog.content);

return (
<div className="cursor-pointer shadow rounded-lg p-4 bg-[#F4F4F8] min-h-[30px]">
<div
className={` cursor-pointer shadow rounded-lg p-4 min-h-[30px] ${
isDark
? "dark: bg-[#23272f] text-white border border-opacity-30"
: "bg-[#F4F4F8]"
}`}
>
<div
className="flex justify-between items-center"
className="flex justify-between items-center overflow-hidden text-ellipsis truncate "
onClick={() => {
onClickTodayDetailBlog(blog.id, blog.title, blog.content);
}}
>
<span className="font-bold">{blog.title}</span>
<span className="text-[12px] opacity-[0.6]">이동하기</span>
<div className="flex gap-[20px] ">
{previewImageUrl && (
<img
src={previewImageUrl}
alt="Blog Preview Image"
className="w-[100px] h-[100px] rounded object-cover"
/>
)}
<div className="flex flex-col gap-[5px]">
<div className="text-[12px] opacity-[0.4]">
<span>{formatDate(blog.date)}</span>
</div>
<div className="font-bold text-[20px]">
<span>{blog.title}</span>
</div>
<div className="text-[15px] opacity-[0.75] max-h-[30px] overflow-hidden">
<span dangerouslySetInnerHTML={{ __html: contentWithoutImage }} />
</div>
</div>
</div>
</div>
</div>
);
Expand Down
Loading

0 comments on commit 74b6f58

Please sign in to comment.