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

Notice 페이지 작성 및 CRUD 기능 구현 #7

Merged
merged 20 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"@lukemorales/query-key-factory": "^1.3.4",
"@supabase/supabase-js": "^2.40.0",
"@tanstack/react-query": "^5.28.14",
"@types/dompurify": "^3.0.5",
"dayjs": "^1.11.10",
"dompurify": "^3.1.0",
"github-label-sync": "^2.3.1",
"jotai": "^2.7.0",
"quill-delta-to-html": "^0.12.1",
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Login from "./pages/Login";
import Home from "./pages/Home";
import Footer from "./components/Footer";
import WriteNotice from "./pages/Notice/WriteNotice";
import EditNotice from "./pages/Notice/EditNotice";
import WriteRecord from "./pages/Record/WriteRecord";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import DetailNotice from "./pages/Notice/DetailNotice";
Expand All @@ -29,6 +30,7 @@ function App() {
<Route path="/notice" element={<Notice />} />
<Route path="/notice/:id" element={<DetailNotice />} />
<Route path="/notice/write" element={<WriteNotice />} />
<Route path="/notice/:id/edit" element={<EditNotice />} />
<Route path="/wod" element={<Wod />} />
<Route path="/record" element={<Record />} />
<Route path="/record/write" element={<WriteRecord />} />
Expand Down
9 changes: 7 additions & 2 deletions src/components/Components.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
}

.line {
height: 2px;
border-bottom: 1px solid #e8e8e8;
margin: 3px 3px 23px 3px;
margin: 10px 0 20px 0;
}

.errorMessage {
color: #be2e22;
font-size: 15px;
font-weight: 500;
}
56 changes: 56 additions & 0 deletions src/components/EditorModules.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const modules = {
toolbar: {
container: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ font: [] }],
[{ align: [] }],
["bold", "italic", "underline", "strike", "blockquote"],
[{ list: "ordered" }, { list: "bullet" }, "link"],
[
{
color: [
"#000000",
"#e60000",
"#ff9900",
"#ffff00",
"#008a00",
"#0066cc",
"#9933ff",
"#ffffff",
"#facccc",
"#ffebcc",
"#ffffcc",
"#cce8cc",
"#cce0f5",
"#ebd6ff",
"#bbbbbb",
"#f06666",
"#ffc266",
"#ffff66",
"#66b966",
"#66a3e0",
"#c285ff",
"#888888",
"#a10000",
"#b26b00",
"#b2b200",
"#006100",
"#0047b2",
"#6b24b2",
"#444444",
"#5c0000",
"#663d00",
"#666600",
"#003700",
"#002966",
"#3d1466",
"custom-color",
],
},
{ background: [] },
],
["image", "video"],
["clean"],
],
},
};
5 changes: 5 additions & 0 deletions src/components/Line.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.line {
height: 2px;
border-bottom: 1px solid #e8e8e8;
margin: 3px 3px 23px 3px;
}
21 changes: 21 additions & 0 deletions src/components/NoticeItem.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.notice_article {
display: flex;
cursor: pointer;
margin-bottom: 35px;
cursor: pointer;
}

.notice_article_title {
font-size: 23px;
font-weight: bold;
margin-bottom: 5px;
}

.notice_article_date,
.notice_article_content_preview {
margin-left: 5px;
}

.notice_article_content_preview {
width: 100vh;
}
40 changes: 40 additions & 0 deletions src/components/NoticeItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import "./NoticeItem.css";
import { Database } from "../api/supabase/supabase";
import DOMPurify from "dompurify";
import { useNavigate } from "react-router-dom";
import Line from "./Line";
import dayjs from "dayjs";

type Notice = Database["public"]["Tables"]["notice"]["Row"];

const NoticeItem = ({ id, title, createdDate, content }: Notice) => {
const navigate = useNavigate();

const onClickMoveToDetail = (id: number) => {
navigate(`/notice/${id}`);
};

return (
<div>
<article
className="notice_article"
onClick={() => onClickMoveToDetail(id)}
>
<div>
<div className="notice_article_title">{title}</div>
<span className="notice_article_date">
{dayjs(createdDate).format("YYYY.MM.DD. HH:mm")}
</span>
<Line />
<div
className="notice_article_content_preview"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(String(content)),
}}
/>
</div>
</article>
</div>
);
};
export default NoticeItem;
32 changes: 32 additions & 0 deletions src/components/WriteNoticeForm.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.write_wrapper {
padding: 50px 300px;
}

.write_btn_wrapper {
display: flex;
flex-direction: row;
}

.write_btn_submit {
margin-left: 20px;
}

.write_title {
width: 100%;
height: 50px;
border: none;
border-bottom: 2px solid gray;
margin-bottom: 10px;
padding-bottom: 7px;
outline: none;
font-size: 30px;
font-weight: bold;
}

.write_title:focus {
border-bottom: 3px solid black;
}

.write_content_reactQuill {
height: 500px;
}
136 changes: 136 additions & 0 deletions src/components/WriteNoticeForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// TODO: 임시저장 기능

import "react-quill/dist/quill.snow.css";
import { SubmitHandler, useForm, useController } from "react-hook-form";
import "./WriteNoticeForm.css";
import Line from "./Line";
import { supabase } from "../api/supabase/supabaseClient";
import dayjs from "dayjs";
import ReactQuill from "react-quill";
import { modules } from "./EditorModules";
import { Database } from "../api/supabase/supabase";
import { useNavigate, useParams } from "react-router-dom";
import { useEffect, useRef } from "react";
import { handleSupabaseResponse } from "../utils/handleSupabaseResponse";
import { useMutation } from "@tanstack/react-query";
import { PostgrestSingleResponse } from "@supabase/supabase-js";

type Notice = Database["public"]["Tables"]["notice"]["Row"];

interface WriteProps {
isEdit: boolean;
data?: Notice[];
syg0629 marked this conversation as resolved.
Show resolved Hide resolved
}

const WriteNoticeForm = (props: WriteProps) => {
const params = useParams();
const noticeId = Number(params.id);
const navigate = useNavigate();
const quillRef = useRef<ReactQuill>(null);

const saveNotice = useMutation<Notice, Error, Notice>({
mutationFn: async (noticeData: Notice): Promise<Notice> => {
const { title, content, writer, createdDate } = noticeData;
const savedNotice: PostgrestSingleResponse<Notice[]> = props.isEdit
? await supabase
.from("notice")
.update({ title, content, createdDate })
.eq("id", noticeId)
.select()
: await supabase
.from("notice")
.insert([{ title, content, writer, createdDate }])
.select();

const formattedData = await handleSupabaseResponse(savedNotice);
syg0629 marked this conversation as resolved.
Show resolved Hide resolved
return formattedData[0];
},
onSuccess: async (savedNotice: Notice): Promise<void> => {
const savedNoticeId: number = savedNotice.id;
if (savedNoticeId) {
navigate(`/notice/${savedNoticeId}`);
}
},
onError: (error) => {
if (error instanceof Error) {
console.log("공지사항 등록/수정 시 오류 >> ", error.message);
}
},
});

const {
register,
formState: { errors },
handleSubmit,
control,
} = useForm<Notice>();

const {
field: { value, onChange },
} = useController({ name: "content", control, rules: { required: true } });

useEffect(() => {
onChange(props.data?.[0].content || "");
syg0629 marked this conversation as resolved.
Show resolved Hide resolved
}, [props.data, onChange]);

const onChangeContents = (content: string) => {
onChange(content === "<p><br></p>" ? "" : content);
};

const onClickSubmit: SubmitHandler<Notice> = (data: Notice) => {
const quillEditor = quillRef.current?.getEditor();
const content = quillEditor?.getContents();

const formattedData = {
...data,
content: JSON.stringify(content),
createdDate: dayjs().format("YYYY.MM.DD HH:mm:ss"),
writer: "작성자",
};

const confirmMessage = props.isEdit
? "수정하시겠습니까?"
: "등록하시겠습니까?";

if (confirm(confirmMessage)) {
saveNotice.mutate(formattedData);
}
};

return (
<div className="write_wrapper">
<form onSubmit={handleSubmit(onClickSubmit)}>
<div className="write_btn_wrapper">
<button>임시저장</button>
<button className="write_btn_submit">
{props.isEdit ? "수정하기" : "등록하기"}
</button>
</div>
<Line />
<input
type="text"
placeholder="제목을 입력하세요."
className="write_title"
{...register("title", { required: true })}
defaultValue={props.data?.[0].title}
/>
{errors?.title?.type === "required" && (
<div className="errorMessage">제목을 입력해주세요.</div>
)}
{errors?.content?.type === "required" && (
<div className="errorMessage">본문 내용을 입력해주세요.</div>
)}
<br />
<ReactQuill
value={value || ""}
onChange={onChangeContents}
placeholder="본문 내용을 입력하세요."
className="write_content_reactQuill"
modules={modules}
ref={quillRef}
/>
</form>
</div>
);
};
export default WriteNoticeForm;
2 changes: 1 addition & 1 deletion src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import crossfit2 from "../assets/crossfit2.jpg";
import crossfit3 from "../assets/crossfit3.jpg";
import "./Home.css";
import Wod from "./Wod";
import Notice from "./Notice";
import Notice from "./Notice/Notice";

const Home = () => {
return (
Expand Down
Loading