Skip to content

Commit 75dede1

Browse files
committed
[week7/mission] Tag 추가 삭제 및 LP 게시글 생성 기능 추가
1 parent 03ab34d commit 75dede1

File tree

7 files changed

+147
-17
lines changed

7 files changed

+147
-17
lines changed

Juno/Week7/Week7_Mission1/src/App.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import NotFoundPage from "./pages/NotFoundPage";
1414
import ProtectedLayout from "./layouts/ProtectedLayout";
1515
import GoogleLoginRedirectPage from "./pages/GoogleLoginRedirectPage";
1616
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
17-
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
18-
import CreateLpPage from "./pages/CreateLpPage";
1917
import LpDetailPage from "./pages/LpDetailPage";
2018

2119
// 1. 홈페이지
@@ -65,10 +63,6 @@ const protectedRoutes: RouteObject[] = [
6563
path: "my",
6664
element: <MyPage />,
6765
},
68-
{
69-
path: "createLp",
70-
element: <CreateLpPage />,
71-
},
7266
],
7367
},
7468
];

Juno/Week7/Week7_Mission1/src/apis/lp.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { PaginationDto } from "../types/common";
22
import type {
33
RequestLpCommentsDto,
4+
RequestLpDto,
45
ResponseLikeLpDto,
56
ResponseLpCommentsDto,
67
ResponseLpDetailDto,
8+
ResponseLpDto,
79
ResponseLpListDto,
810
} from "../types/lp";
911
import { axiosInstance } from "./axios";
@@ -48,3 +50,11 @@ export const deleteLike = async (lpId: number): Promise<ResponseLikeLpDto> => {
4850

4951
return data;
5052
};
53+
54+
export const postLp = async (
55+
requestLpDto: RequestLpDto
56+
): Promise<ResponseLpDto> => {
57+
const { data } = await axiosInstance.post("v1/lps", requestLpDto);
58+
59+
return data;
60+
};

Juno/Week7/Week7_Mission1/src/components/LpModal.tsx

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { useState } from "react";
2+
import TagCard from "./TagCard";
3+
import type { Tag } from "../types/lp";
4+
import usePostLp from "../hooks/mutations/usePostLp";
25

36
interface LpModalProps {
47
isOpen: boolean;
@@ -9,6 +12,31 @@ interface LpModalProps {
912
const LpModal = ({ isOpen, onClose, toggle }: LpModalProps) => {
1013
const [name, setName] = useState("");
1114
const [content, setContent] = useState("");
15+
const [thumnailFile, setThumnailFile] = useState<File | null>(null);
16+
const [tagName, setTagName] = useState<string>("");
17+
const [tags, setTags] = useState<Tag[]>([]);
18+
19+
const { mutate } = usePostLp();
20+
21+
const handlePostLp = () => {
22+
mutate({
23+
title: name,
24+
content: content,
25+
thumbnail: thumnailFile?.name, // 파일 경로를 파일 이름으로 대체
26+
tags: tags.map((tag) => tag.name),
27+
published: true,
28+
});
29+
onClose();
30+
};
31+
32+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
33+
const file = e.target.files?.[0] || null;
34+
setThumnailFile(file);
35+
};
36+
37+
const deleteTag = (idToDelete: number) => {
38+
setTags((prev) => prev.filter((tag) => tag.id !== idToDelete));
39+
};
1240

1341
return (
1442
<>
@@ -19,7 +47,7 @@ const LpModal = ({ isOpen, onClose, toggle }: LpModalProps) => {
1947
onClick={onClose}
2048
></div>
2149
<div
22-
className={`fixed inset-x-1/4 inset-y-1/6 rounded-2xl flex flex-col items-center space-y-4 box-border px-4 py-2 bg-gray-50 shadow-2xl z-40 ${
50+
className={`fixed inset-x-1/4 inset-y-24 rounded-2xl flex flex-col items-center space-y-4 box-border px-4 py-2 bg-gray-50 shadow-2xl z-40 ${
2351
isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
2452
}`}
2553
>
@@ -36,24 +64,60 @@ const LpModal = ({ isOpen, onClose, toggle }: LpModalProps) => {
3664
htmlFor="upload-lp-image"
3765
className="w-40 h-40 rounded-full bg-gray-500"
3866
></label>
39-
<input type="file" className="hidden" id="upload-lp-image" />
67+
<input
68+
type="file"
69+
className="hidden"
70+
id="upload-lp-image"
71+
onChange={handleFileChange}
72+
accept="image/*"
73+
/>
4074
<input
4175
type="text"
4276
onChange={(e) => setName(e.target.value)}
4377
value={name}
4478
placeholder="Lp Name"
45-
className="pl-4 py-2 w-full border border-gray-950 rounded-xl"
79+
className="pl-4 py-2 w-full border border-gray-950 rounded-lg"
4680
/>
4781
<input
4882
type="text"
4983
value={content}
5084
onChange={(e) => setContent(e.target.value)}
5185
placeholder="Lp Content"
52-
className="pl-4 py-2 w-full border border-gray-950 rounded-xl"
86+
className="pl-4 py-2 w-full border border-gray-950 rounded-lg"
5387
/>
88+
<div className="flex w-full space-x-2">
89+
<input
90+
type="text"
91+
value={tagName}
92+
onChange={(e) => setTagName(e.target.value)}
93+
placeholder="Lp Tag"
94+
className="pl-4 py-2 w-full border border-gray-950 rounded-lg"
95+
/>
96+
<button
97+
disabled={tagName.length === 0}
98+
className="cursor-pointer py-2 px-4 bg-gray-950 text-gray-50 rounded-lg disabled:bg-gray-400 disabled:cursor-not-allowed"
99+
onClick={() => {
100+
setTags((prev) => [...prev, { name: tagName, id: Date.now() }]);
101+
setTagName("");
102+
}}
103+
>
104+
Add
105+
</button>
106+
</div>
107+
<div className="flex w-full space-x-2 overflow-x-scroll">
108+
{tags.map((tag) => (
109+
<TagCard
110+
name={tag.name}
111+
id={tag.id}
112+
onDelete={deleteTag}
113+
key={tag.id}
114+
/>
115+
))}
116+
</div>
54117
<button
55118
disabled={name.length === 0 || content.length === 0}
56-
className="cursor-pointer py-2 w-full bg-gray-950 text-gray-50 rounded-xl disabled:bg-gray-400 disabled:cursor-not-allowed"
119+
className="cursor-pointer py-2 w-full bg-gray-950 text-gray-50 rounded-lg disabled:bg-gray-400 disabled:cursor-not-allowed"
120+
onClick={handlePostLp}
57121
>
58122
Add Lp
59123
</button>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
interface TagProps {
2+
name: string;
3+
id: number;
4+
onDelete: (id: number) => void;
5+
}
6+
7+
const TagCard = ({ name, id, onDelete }: TagProps) => {
8+
const handleDelete = () => {
9+
onDelete(id);
10+
};
11+
12+
return (
13+
<div className="flex space-x-2 py-1 px-2 items-center justify-center border border-gray-950 rounded-lg">
14+
<span>{name}</span>
15+
<button onClick={handleDelete} className="cursor-pointer">
16+
x
17+
</button>
18+
</div>
19+
);
20+
};
21+
22+
export default TagCard;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import { postLp } from "../../apis/lp";
3+
import { queryClient } from "../../App";
4+
import { QUERY_KEY } from "../../constants/key";
5+
6+
const usePostLp = () => {
7+
return useMutation({
8+
mutationFn: postLp,
9+
onSuccess: (data) => {
10+
queryClient.invalidateQueries({
11+
queryKey: [QUERY_KEY.lps, data.data.id],
12+
});
13+
},
14+
});
15+
};
16+
17+
export default usePostLp;

Juno/Week7/Week7_Mission1/src/layouts/ProtectedLayout.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { Navigate, Outlet } from "react-router-dom";
22
import { useAuth } from "../context/AuthContext";
33
import Header from "../components/Header";
44
import Navbar from "../components/Navbar";
5-
import FloatingModalButton from "../components/FloatingLpModalButton";
65
import useToggle from "../hooks/useToggle";
76

87
function ProtectedLayout() {
9-
const { isOpen, toggle, close } = useToggle();
8+
const {
9+
isOpen: isNavbarOpen,
10+
toggle: toggleNavbar,
11+
close: closeNavbar,
12+
} = useToggle();
1013

1114
const { accessToken } = useAuth();
1215

@@ -18,12 +21,13 @@ function ProtectedLayout() {
1821

1922
return (
2023
<div className="min-h-screen w-full flex flex-col bg-gray-50">
21-
<Header toggleNavbar={toggle} />
24+
<Header toggleNavbar={toggleNavbar} />
2225
<div className="flex flex-1">
23-
<Navbar isOpen={isOpen} onClose={close} />
26+
<Navbar isOpen={isNavbarOpen} onClose={closeNavbar} />
2427
<main className="relative flex-1 mt-15">
25-
<Outlet />
26-
<FloatingModalButton />
28+
<div className="p-6">
29+
<Outlet />
30+
</div>
2731
</main>
2832
</div>
2933
</div>

Juno/Week7/Week7_Mission1/src/types/lp.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,22 @@ export type ResponseLikeLpDto = CommonResponse<{
7474
userId: number;
7575
lpId: number;
7676
}>;
77+
78+
export type RequestLpDto = {
79+
title: string;
80+
content: string;
81+
thumbnail?: string;
82+
tags: string[];
83+
published: boolean;
84+
};
85+
86+
export type ResponseLpDto = CommonResponse<{
87+
id: number;
88+
title: string;
89+
content: string;
90+
thumnail: string;
91+
published: boolean;
92+
authorId: number;
93+
updatedAt: string;
94+
createdAt: string;
95+
}>;

0 commit comments

Comments
 (0)