diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..9ce71ea --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +# GitHub Pages에 정적 콘텐츠를 배포하기 위한 간단한 워크플로우 +name: Deploy static content to Pages + +on: + # 기본 브랜치에 대한 푸시 이벤트 발생 시 실행 + push: + branches: ['main'] + + # Actions 탭에서 수동으로 워크플로우를 실행할 수 있도록 구성 + workflow_dispatch: + +# GITHUB_TOKEN의 권한을 설정하여 GitHub Pages에 배포할 수 있도록 함 +permissions: + contents: read + pages: write + id-token: write + +# 동시에 하나의 배포만 허용하도록 구성 +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + # 단순히 배포만 수행하기에 하나의 잡으로만 구성 + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Build + run: npm run build + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + # dist 디렉터리 업로드 + path: './dist' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v3 diff --git a/package-lock.json b/package-lock.json index e3d6053..651faee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.40.0", + "@tanstack/react-query-devtools": "^5.40.0", "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1100,6 +1102,55 @@ "node": ">=14" } }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.37.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.37.1.tgz", + "integrity": "sha512-XcG4IIHIv0YQKrexTqo2zogQWR1Sz672tX2KsfE9kzB+9zhx44vRKH5si4WDILE1PIWQpStFs/NnrDQrBAUQpg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.0.tgz", + "integrity": "sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.40.0.tgz", + "integrity": "sha512-JoQOQj/LKaHoHVMAh73R0pc4lIAHiZMV8L4DGHsTSvHcKi22LZmSC9aYBY9oMkqGpFtKmbspwrUIn55+czNSbA==", + "dependencies": { + "@tanstack/query-devtools": "5.37.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.40.0", + "react": "^18 || ^19" + } + }, "node_modules/@types/history": { "version": "4.7.11", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", diff --git a/package.json b/package.json index 73781ed..1eee78e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.40.0", + "@tanstack/react-query-devtools": "^5.40.0", "axios": "^1.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index 70925be..ba662e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,21 +6,28 @@ import Post from './pages/Post'; import Resume from './pages/Resume'; import Write from './pages/Write'; import Header from './components/Header'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; function App() { + const queryClient = new QueryClient(); + return ( - - - }> - }> - } /> - } /> + + + + + }> + }> + } /> + } /> + + } /> - {/*todo (3-3) Post 추가*/} - - {/*todo (5-1) Write 추가*/} - - + } /> + + + ); } diff --git a/src/api/index.ts b/src/api/index.ts index cfe4414..1e82181 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,31 +1,37 @@ -import axios from 'axios'; -import { IResponsePostList } from './types'; +import axios, { AxiosResponse } from 'axios'; +import { IPost, IResponsePostList, TAG } from './types'; const instance = axios.create({ headers: { 'Content-Type': 'application/json', }, - baseURL: '', + baseURL: 'http://34.64.250.51:8080/', }); -// todo (6) api 작성 - -export const getPostList = () => { - return null; +export const getPostList = (): Promise> => { + return instance.get('/posts'); }; -export const createPost = () => { - return null; +export const createPost = (title: string, contents: string, tag: TAG) => { + return instance.post('/posts', { + title, + contents, + tag, + }); }; -export const getPostById = () => { - return null; +export const getPostById = (id: string): Promise> => { + return instance.get(`/posts/${id}`); }; -export const updatePostById = () => { - return null; +export const updatePostById = (id: string, title: string, contents: string, tag: TAG) => { + return instance.put(`/posts/${id}`, { + title, + contents, + tag, + }); }; -export const deletePostById = () => { - return null; -}; +export const deletePostById = (id: string) => { + return instance.delete(`/posts/${id}`); +}; \ No newline at end of file diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 14a002f..d528264 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -28,7 +28,7 @@ const Header = () => {
-

kimsudal.log

+

aaaaaa

diff --git a/src/components/PostListItem.tsx b/src/components/PostListItem.tsx index d57920c..57349f7 100644 --- a/src/components/PostListItem.tsx +++ b/src/components/PostListItem.tsx @@ -23,7 +23,14 @@ const Contents = styled.p` `; const PostListItem = (props: IPost) => { - return
{/*todo (3-2) 게시글 목록 아이템 작성*/}
; + const { id, title, contents, tag } = props; + return ( + +

{title}

+ {contents} + #{tag} +
+ ); }; export default PostListItem; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 3e4f911..ee7e53d 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,38 +1,32 @@ import { useEffect, useState } from 'react'; import { getPostList } from '../api'; import PostListItem from '../components/PostListItem'; -import { IResponsePostList, TAG } from '../api/types'; +import { IResponsePostList } from '../api/types'; import NoPostList from '../components/NoPostList'; -const list = [ - { - post: { - id: 1, - title: '1번 게시글', - contents: '내용', - tag: TAG.REACT, - }, - }, - { - post: { - id: 2, - title: '2번 게시글', - contents: '내용', - tag: TAG.REACT, - }, - }, - { - post: { - id: 3, - title: '3번 게시글', - contents: '내용', - tag: TAG.REACT, - }, - }, -]; - const Home = () => { - return
{/*todo (3-1) post 목록 작성*/}
; + const [postList, setPostList] = useState([]); + + const fetchPostList = async () => { + const { data } = await getPostList(); + setPostList(data); + }; + + useEffect(() => { + fetchPostList(); + }, []); + + if (postList.length === 0) { + return ; + } + + return ( +
+ {postList.map(item => ( + + ))} +
+ ); }; export default Home; diff --git a/src/pages/Layout.tsx b/src/pages/Layout.tsx index d6ebd90..3994017 100644 --- a/src/pages/Layout.tsx +++ b/src/pages/Layout.tsx @@ -58,16 +58,21 @@ const Layout = () => { return (
- {/*todo (1) 프로필 꾸미기*/}
- +
- 이름 - 설명 + 조영민 + 2024 실전코딩
- +
); diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx index 78faa04..4469f8b 100644 --- a/src/pages/Post.tsx +++ b/src/pages/Post.tsx @@ -1,10 +1,9 @@ -import { useEffect, useState } from 'react'; -import { useParams, Link, useNavigate } from 'react-router-dom'; +import { useParams, Link } from 'react-router-dom'; import styled from '@emotion/styled'; -import { deletePostById, getPostById } from '../api'; -import { IPost } from '../api/types'; import NotFound from '../components/NotFound'; import Tag from '../components/Tag'; +import useGetPostById from '../queries/useGetPostById.ts'; +import useDeletePostById from '../queries/useDeletePostById.ts'; const Title = styled.h1` font-size: 3rem; @@ -60,8 +59,54 @@ const Text = styled.p` `; const Post = () => { - // todo (4) post 컴포넌트 작성 - return
; + const params = useParams(); + const { postId = '' } = params; + const { data: post, isError, isLoading } = useGetPostById(postId); + const { mutate: deletePost } = useDeletePostById(); + + const clickDeleteButton = () => { + const result = window.confirm('정말로 게시글을 삭제하시겠습니까?'); + if (result) { + deletePost({ postId }); + } + }; + + if (isLoading) { + return
...불러오는 중...
; + } + + if (!post || isError) { + return ; + } + + return ( +
+
+ {post?.title} + + +
n분전
+
+
+ + 수정 + + 삭제 +
+
+ {post?.tag && ( + + #{post.tag} + + )} +
+ + {post?.contents?.split('\n').map((text, index) => ( + {text} + ))} + +
+ ); }; export default Post; diff --git a/src/pages/Resume.tsx b/src/pages/Resume.tsx index 4f2c005..51c2f4d 100644 --- a/src/pages/Resume.tsx +++ b/src/pages/Resume.tsx @@ -1,5 +1,23 @@ const Resume = () => { - return
나는 프로젝트 내역
; + return

진행했던 수업

+
+

2023.09~2023.12

+
+ 웹프로그래밍 수강 +
+
+
+

2024.03~2024.06

+
+ 실전코딩 수강 +
+
+ +

자랑거리가 없네!

+
+
; + + }; export default Resume; diff --git a/src/pages/Write.tsx b/src/pages/Write.tsx index cbc0503..5d8f71f 100644 --- a/src/pages/Write.tsx +++ b/src/pages/Write.tsx @@ -1,8 +1,10 @@ import { ChangeEvent, useEffect, useState } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import styled from '@emotion/styled'; -import { createPost, getPostById, updatePostById } from '../api'; import { TAG } from '../api/types'; +import useCreatePost from '../queries/useCreatePost.ts'; +import useUpdatePostById from '../queries/useUpdatePostById.ts'; +import useGetPostById from '../queries/useGetPostById.ts'; const TitleInput = styled.input` display: block; @@ -85,12 +87,68 @@ const SaveButton = styled.button` `; const Write = () => { - // todo (5) 게시글 작성 페이지 만들기 + const { state } = useLocation(); + const isEdit = state?.postId; + const navigate = useNavigate(); + const { data: post, isSuccess: isSuccessFetchPost } = useGetPostById(state?.postId); + const { mutate: createPost } = useCreatePost(); + const { mutate: updatePost } = useUpdatePostById(); + + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [tag, setTag] = useState(TAG.REACT); + const tagList = Object.keys(TAG); + + const handleChangeTitle = (event: ChangeEvent) => { + setTitle(event.target.value); + }; + const handleChangeContent = (event: ChangeEvent) => { + setContent(event.target.value); + }; + + const handleChangeTag = (event: ChangeEvent) => { + setTag(event.target.value as TAG); + }; + + const clickConfirm = () => { + if (!title || !content) { + alert('빈 값이 있습니다.'); + return; + } + + if (isEdit) { + updatePost({ postId: state.postId, title, contents: content, tag }); + } else { + createPost({ title, contents: content, tag }); + } + navigate('/'); + }; + + useEffect(() => { + if (isSuccessFetchPost) { + setTitle(post.title); + setContent(post.contents); + setTag(post.tag); + } + }, [isSuccessFetchPost]); + return (
- 나는 글쓰기 -
{/*todo (5-2) 제목 / 태그 셀렉 / 내용 입력란 추가*/}
- {/*todo (5-3) 나가기, 저장하기 버튼 추가*/} +
+ + + {tagList.map(tag => { + return ; + })} + + +
+ + + 나가기 + + 저장하기 +
); }; diff --git a/src/queries/useCreatePost.ts b/src/queries/useCreatePost.ts new file mode 100644 index 0000000..80d2a50 --- /dev/null +++ b/src/queries/useCreatePost.ts @@ -0,0 +1,16 @@ +import { createPost } from '../api'; +import { useMutation } from '@tanstack/react-query'; +import { TAG } from '../api/types.ts'; + +const useCreatePost = () => { + const mutation = async ({ title, contents, tag }: { title: string; contents: string; tag: TAG }) => { + await createPost(title, contents, tag); + }; + + return useMutation({ + mutationKey: ['createPost'], + mutationFn: mutation, + }); +}; + +export default useCreatePost; \ No newline at end of file diff --git a/src/queries/useDeletePostById.ts b/src/queries/useDeletePostById.ts new file mode 100644 index 0000000..a7bb840 --- /dev/null +++ b/src/queries/useDeletePostById.ts @@ -0,0 +1,24 @@ +import { deletePostById } from '../api'; +import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; + +const useDeletePostById = () => { + const navigate = useNavigate(); + + const mutate = async ({ postId }: { postId: string }) => { + const { data } = await deletePostById(postId); + return data; + }; + + return useMutation({ + mutationFn: mutate, + onSuccess: () => { + navigate('/'); + }, + onError: () => { + alert('게시글 삭제에 실패하였습니다. 잠시 후 다시 시도해주세요.'); + }, + }); +}; + +export default useDeletePostById; \ No newline at end of file diff --git a/src/queries/useGetPostById.ts b/src/queries/useGetPostById.ts new file mode 100644 index 0000000..9ef408f --- /dev/null +++ b/src/queries/useGetPostById.ts @@ -0,0 +1,17 @@ +import { getPostById } from '../api'; +import { useQuery } from '@tanstack/react-query'; + +const useGetPostById = (postId: string) => { + const fetcher = async () => { + const { data } = await getPostById(postId); + return data; + }; + + return useQuery({ + queryKey: ['getPostListById', postId], + queryFn: fetcher, + enabled: !!postId, + }); +}; + +export default useGetPostById; \ No newline at end of file diff --git a/src/queries/useGetPostList.ts b/src/queries/useGetPostList.ts new file mode 100644 index 0000000..df8e175 --- /dev/null +++ b/src/queries/useGetPostList.ts @@ -0,0 +1,16 @@ +import { getPostList } from '../api'; +import { useQuery } from '@tanstack/react-query'; + +const useGetPostList = () => { + const fetcher = async () => { + const { data } = await getPostList(); + return data; + }; + + return useQuery({ + queryKey: ['getPostList'], + queryFn: fetcher, + }); +}; + +export default useGetPostList; \ No newline at end of file diff --git a/src/queries/useUpdatePostById.ts b/src/queries/useUpdatePostById.ts new file mode 100644 index 0000000..1f0016e --- /dev/null +++ b/src/queries/useUpdatePostById.ts @@ -0,0 +1,16 @@ +import { updatePostById } from '../api'; +import { useMutation } from '@tanstack/react-query'; +import { TAG } from '../api/types.ts'; + +const useUpdatePostById = () => { + const mutation = async ({ postId, title, contents, tag }: { postId: string; title: string; contents: string; tag: TAG }) => { + await updatePostById(postId, title, contents, tag); + }; + + return useMutation({ + mutationKey: ['updatePost'], + mutationFn: mutation, + }); +}; + +export default useUpdatePostById; \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 627a319..f49dabb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,5 +3,6 @@ import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ + base: '/CNU_Blog/', plugins: [react()], });