diff --git a/.pnp.cjs b/.pnp.cjs index 4e6ae80..3955f3b 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -3987,6 +3987,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/js-cookie", [\ + ["npm:3.0.6", {\ + "packageLocation": "./.yarn/cache/@types-js-cookie-npm-3.0.6-c9126e5b48-173afaf5ea.zip/node_modules/@types/js-cookie/",\ + "packageDependencies": [\ + ["@types/js-cookie", "npm:3.0.6"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/json-schema", [\ ["npm:7.0.15", {\ "packageLocation": "./.yarn/cache/@types-json-schema-npm-7.0.15-fd16381786-a996a745e6.zip/node_modules/@types/json-schema/",\ @@ -6190,6 +6199,7 @@ const RAW_RUNTIME_STATE = ["@react-three/drei", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:9.88.13"],\ ["@react-three/fiber", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:8.15.10"],\ ["@react-three/postprocessing", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:2.15.11"],\ + ["@types/js-cookie", "npm:3.0.6"],\ ["@types/node", "npm:20.9.2"],\ ["@types/react", "npm:18.2.37"],\ ["@types/react-dom", "npm:18.2.15"],\ @@ -6203,7 +6213,9 @@ const RAW_RUNTIME_STATE = ["eslint", "npm:8.53.0"],\ ["eslint-plugin-react-hooks", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:4.6.0"],\ ["eslint-plugin-react-refresh", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:0.4.4"],\ + ["js-cookie", "npm:3.0.5"],\ ["leva", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:0.9.35"],\ + ["lucide-react", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:0.294.0"],\ ["react", "npm:18.2.0"],\ ["react-dom", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:18.2.0"],\ ["react-markdown", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:9.0.1"],\ @@ -9799,6 +9811,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["js-cookie", [\ + ["npm:3.0.5", {\ + "packageLocation": "./.yarn/cache/js-cookie-npm-3.0.5-8fc8fcc9b4-04a0e56040.zip/node_modules/js-cookie/",\ + "packageDependencies": [\ + ["js-cookie", "npm:3.0.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["js-tokens", [\ ["npm:4.0.0", {\ "packageLocation": "./.yarn/cache/js-tokens-npm-4.0.0-0ac852e9e2-e248708d37.zip/node_modules/js-tokens/",\ @@ -10313,6 +10334,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["lucide-react", [\ + ["npm:0.294.0", {\ + "packageLocation": "./.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-3c8d05743c.zip/node_modules/lucide-react/",\ + "packageDependencies": [\ + ["lucide-react", "npm:0.294.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:0.294.0", {\ + "packageLocation": "./.yarn/__virtual__/lucide-react-virtual-163e38136d/0/cache/lucide-react-npm-0.294.0-86f24c8068-3c8d05743c.zip/node_modules/lucide-react/",\ + "packageDependencies": [\ + ["lucide-react", "virtual:658502eb4296e93abedc18b6aa9b26978f434f08d98e21ebb0e725354b8bb54b62db9c4a1893e460c694ff7500ff5cbafa4457b0dfd26b5838868666c861e990#npm:0.294.0"],\ + ["@types/react", "npm:18.2.37"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["maath", [\ ["npm:0.6.0", {\ "packageLocation": "./.yarn/cache/maath-npm-0.6.0-bf6e867013-a9b716841d.zip/node_modules/maath/",\ @@ -13265,7 +13308,6 @@ const RAW_RUNTIME_STATE = ["eslint", "npm:8.53.0"],\ ["eslint-config-prettier", "virtual:2499dbb93d824027565d71b0716c4fb8b548ad61955d0a0286bfb3c5b4058e227894b6691d96808c00f576db14870018375210362c26ee321ea99fd6ed041c74#npm:9.0.0"],\ ["eslint-plugin-prettier", "virtual:2499dbb93d824027565d71b0716c4fb8b548ad61955d0a0286bfb3c5b4058e227894b6691d96808c00f576db14870018375210362c26ee321ea99fd6ed041c74#npm:5.0.1"],\ - ["form-data", "npm:4.0.0"],\ ["ioredis", "npm:5.3.2"],\ ["jest", "virtual:2499dbb93d824027565d71b0716c4fb8b548ad61955d0a0286bfb3c5b4058e227894b6691d96808c00f576db14870018375210362c26ee321ea99fd6ed041c74#npm:29.7.0"],\ ["mongoose", "npm:8.0.1"],\ diff --git a/.yarn/cache/@types-js-cookie-npm-3.0.6-c9126e5b48-173afaf5ea.zip b/.yarn/cache/@types-js-cookie-npm-3.0.6-c9126e5b48-173afaf5ea.zip new file mode 100644 index 0000000..97ddf14 Binary files /dev/null and b/.yarn/cache/@types-js-cookie-npm-3.0.6-c9126e5b48-173afaf5ea.zip differ diff --git a/.yarn/cache/js-cookie-npm-3.0.5-8fc8fcc9b4-04a0e56040.zip b/.yarn/cache/js-cookie-npm-3.0.5-8fc8fcc9b4-04a0e56040.zip new file mode 100644 index 0000000..c85ff7a Binary files /dev/null and b/.yarn/cache/js-cookie-npm-3.0.5-8fc8fcc9b4-04a0e56040.zip differ diff --git a/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-3c8d05743c.zip b/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-3c8d05743c.zip new file mode 100644 index 0000000..2f67589 Binary files /dev/null and b/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-3c8d05743c.zip differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index c44764b..3b0cfe1 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/Dockerfile-was b/Dockerfile-was index 55dc6ad..f417898 100644 --- a/Dockerfile-was +++ b/Dockerfile-was @@ -4,6 +4,7 @@ WORKDIR /app ADD . /app +RUN SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_arch=x64 npm_config_platform=linux yarn workspace server add sharp RUN yarn workspace server build EXPOSE 3000 diff --git a/docker-compose.yml b/docker-compose.yml index d8c573c..aecaf48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: was: container_name: was image: qkrwogk/web16-b1g1-be:GITHUB_SHA + restart: always ports: - 3000:3000 env_file: @@ -13,6 +14,7 @@ services: web: container_name: web image: qkrwogk/web16-b1g1-fe:GITHUB_SHA + restart: always ports: - 80:80 - 443:443 diff --git a/packages/client/index.html b/packages/client/index.html index eea3d72..6c458c2 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + 별 하나에 글 하나🌟
diff --git a/packages/client/package.json b/packages/client/package.json index b8fcc6f..78d869e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -16,6 +16,7 @@ "@react-three/fiber": "^8.15.10", "@types/react-router-dom": "^5.3.3", "axios": "^1.6.2", + "lucide-react": "^0.294.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", @@ -26,6 +27,7 @@ "devDependencies": { "@emotion/react": "^11.11.1", "@react-three/postprocessing": "^2.15.11", + "@types/js-cookie": "^3.0.6", "@types/node": "^20.9.2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", @@ -37,6 +39,7 @@ "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "js-cookie": "^3.0.5", "leva": "^0.9.35", "three-stdlib": "^2.28.5", "typescript": "^5.0.2", diff --git a/packages/client/src/app/Router.tsx b/packages/client/src/app/Router.tsx index 24bd6fa..3c901c3 100644 --- a/packages/client/src/app/Router.tsx +++ b/packages/client/src/app/Router.tsx @@ -6,12 +6,26 @@ import { import Home from '../pages/Home'; import Landing from '../pages/Landing'; import { WritingModal } from 'features/writingModal'; +import LoginModal from 'widgets/loginModal'; +import SignUpModal from 'widgets/signupModal/SignUpModal'; +import NickNameSetModal from 'widgets/nickNameSetModal/NickNameSetModal'; +import LogoAndStart from 'widgets/logoAndStart'; +import { PostModal } from 'features/postModal'; export const router = createBrowserRouter( createRoutesFromElements( <> - } /> + }> + } /> + } /> + } /> + } /> + + }> + + } /> + } /> , diff --git a/packages/client/src/entities/posts/ui/Post.tsx b/packages/client/src/entities/posts/ui/Post.tsx index 1172751..dbec242 100644 --- a/packages/client/src/entities/posts/ui/Post.tsx +++ b/packages/client/src/entities/posts/ui/Post.tsx @@ -6,11 +6,11 @@ import { Html } from '@react-three/drei'; import styled from '@emotion/styled'; import { useViewStore } from 'shared/store/useViewStore'; import * as THREE from 'three'; -import { PostData } from 'shared/lib/types/post'; -import { usePostStore } from 'shared/store/usePostStore'; +import { StarData } from 'shared/lib/types/star'; +import { useNavigate } from 'react-router-dom'; interface PropsType { - data: PostData; + data: StarData; onClick: () => void; isSelected: boolean; } @@ -19,7 +19,7 @@ export default function Post({ data, onClick, isSelected }: PropsType) { const { targetView, setTargetView } = useCameraStore(); const meshRef = useRef(null!); const { view, setView } = useViewStore(); - const { setData } = usePostStore(); + const navigate = useNavigate(); const handleMeshClick = (e: ThreeEvent) => { e.stopPropagation(); @@ -28,16 +28,19 @@ export default function Post({ data, onClick, isSelected }: PropsType) { if (meshRef.current !== targetView) { setView('DETAIL'); setTargetView(meshRef.current); + navigate(`/home/${data.id}`); return; } - setData(data); + navigate(`/home/${data.id}/detail`); setView('POST'); }; return ( ('star'); return ( <> - {dummyData.map((data, index) => ( - setPost(index)} - isSelected={post === index} - /> - ))} + {data && + data.map((data, index) => ( + setPost(index)} + isSelected={post === index} + /> + ))} ); } - -const dummyData = [ - { - position: new THREE.Vector3(1000, 1000, 1800), - size: 100, - color: 'red', - title: '별글입니당', - content: '# 배가고프다', - images: [ - 'https://github.com/boostcampwm2023/web16-B1G1/assets/35567292/ed205126-f8c5-4f84-98d9-fade19cd6c1d', - ], - }, - { - position: new THREE.Vector3(1300, 500, 1000), - size: 200, - color: 'blue', - title: '별글입니당', - content: '~stroke~', - images: [ - 'https://github.com/boostcampwm2023/web16-B1G1/assets/35567292/ed205126-f8c5-4f84-98d9-fade19cd6c1d', - ], - }, - { - position: new THREE.Vector3(3000, 1000, 2500), - size: 150, - color: 'yellow', - title: '별글입니당', - content: "- I'm hungry", - images: [ - 'https://github.com/boostcampwm2023/web16-B1G1/assets/35567292/ed205126-f8c5-4f84-98d9-fade19cd6c1d', - ], - }, -]; // TODO: 서버로부터 받아온 데이터로 변경 필요 diff --git a/packages/client/src/features/postModal/PostModal.tsx b/packages/client/src/features/postModal/PostModal.tsx deleted file mode 100644 index 54a6294..0000000 --- a/packages/client/src/features/postModal/PostModal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useViewStore } from 'shared/store/useViewStore'; -import { Button, Modal, ModalPortal } from 'shared/ui'; -import { PostData } from 'shared/lib/types/post'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import styled from '@emotion/styled'; - -interface PropsType { - data: PostData; -} - -export default function PostModal({ data }: PropsType) { - const { setView } = useViewStore(); - return ( - - { - setView('DETAIL'); - }} - > - - img - - - - {data.content} - - - - - ); -} - -const rightButton = ( - -); - -const PostModalLayout = styled(Modal)` - transform: translate(-10%, -50%); -`; - -const TextContainer = styled.div` - overflow-y: auto; - width: 807px; - - ${({ theme: { colors } }) => ({ - color: colors.text.secondary, - })} -`; - -const ImageContainer = styled.div` - display: flex; - justify-content: center; - margin-bottom: 26px; -`; - -const Image = styled.img` - width: 649px; -`; diff --git a/packages/client/src/features/postModal/api/deletePost.ts b/packages/client/src/features/postModal/api/deletePost.ts new file mode 100644 index 0000000..0516e04 --- /dev/null +++ b/packages/client/src/features/postModal/api/deletePost.ts @@ -0,0 +1,11 @@ +import instance from 'shared/apis/AxiosInterceptor'; +import { BASE_URL } from '@constants'; + +export const deletePost = async (postId: string) => { + const { data } = await instance({ + method: 'DELETE', + url: `${BASE_URL}/post/${postId}`, + }); + + return data; +}; diff --git a/packages/client/src/features/postModal/index.ts b/packages/client/src/features/postModal/index.ts new file mode 100644 index 0000000..321a58c --- /dev/null +++ b/packages/client/src/features/postModal/index.ts @@ -0,0 +1 @@ +export { default as PostModal } from './ui/PostModal'; diff --git a/packages/client/src/features/postModal/ui/ImageSlider.tsx b/packages/client/src/features/postModal/ui/ImageSlider.tsx new file mode 100644 index 0000000..d70b18f --- /dev/null +++ b/packages/client/src/features/postModal/ui/ImageSlider.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react'; +import { ArrowBigLeft, ArrowBigRight, CircleDot, Circle } from 'lucide-react'; +import styled from '@emotion/styled'; + +interface PropsType { + imageUrls: string[]; +} + +export default function ImageSlider({ imageUrls }: PropsType) { + const [imageIndex, setImageIndex] = useState(0); + + const handlePrev = () => { + setImageIndex((index) => { + if (index === 0) return imageUrls.length - 1; + return index - 1; + }); + }; + + const handleNext = () => { + setImageIndex((index) => { + if (index === imageUrls.length - 1) return 0; + return index + 1; + }); + }; + + const Dots = () => { + return ( + <> + {imageUrls.map((_, index) => ( + setImageIndex(index)}> + {index === imageIndex ? : } + + ))} + + ); + }; + + return ( + + + {imageUrls.map((url) => { + return ; + })} + + + + + + + + ); +} + +const Layout = styled.div` + position: relative; +`; + +const CurrentImage = styled.div` + width: 100%; + height: 100%; + display: flex; + overflow: hidden; +`; + +const Image = styled.img<{ index: number }>` + object-fit: cover; + width: 100%; + height: 100%; + display: block; + flex-shrink: 0; + flex-grow: 0; + translate: ${({ index }) => -100 * index}%; + transition: translate 300ms ease-in-out; +`; + +const Button = styled.button` + all: unset; + display: block; + position: absolute; + top: 0; + bottom: 0; + padding: 16px; + cursor: pointer; + transition: background-color 100ms ease-in-out; + + &:hover { + background-color: rgba(0, 0, 0, 0.2); + } + + > * { + stroke: white; + fill: black; + width: 32px; + height: 32px; + } +`; + +const Pagination = styled.div` + position: absolute; + bottom: 8px; + left: 50%; + translate: -50%; + display: flex; + gap: 4px; +`; + +const Dot = styled.button` + all: unset; + display: block; + cursor: pointer; + width: 16px; + height: 16px; + transition: scale 100ms ease-in-out; + + &:hover { + scale: 1.2; + } + + > * { + stroke: white; + fill: black; + width: 100%; + height: 100%; + } +`; diff --git a/packages/client/src/features/postModal/ui/PostModal.tsx b/packages/client/src/features/postModal/ui/PostModal.tsx new file mode 100644 index 0000000..4bb69f6 --- /dev/null +++ b/packages/client/src/features/postModal/ui/PostModal.tsx @@ -0,0 +1,122 @@ +import { useViewStore } from 'shared/store/useViewStore'; +import { Button, Modal, ModalPortal } from 'shared/ui'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import AlertDialog from 'shared/ui/alertDialog/AlertDialog'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useFetch } from 'shared/hooks'; +import { PostData } from 'shared/lib/types/post'; +import { deletePost } from '../api/deletePost'; +import ImageSlider from './ImageSlider'; + +export default function PostModal() { + const { setView } = useViewStore(); + const [deleteModal, setDeleteModal] = useState(false); + const { postId } = useParams(); + const navigate = useNavigate(); + const { data } = useFetch(`post/${postId}`); + + const rightButton = ( + + ); + + const handleDelete = async () => { + const res = await deletePost(postId!); + setDeleteModal(false); + if (res.status === 200) { + setView('MAIN'); + navigate('/home'); + } else { + alert('글 삭제 실패'); + } + }; + + return ( + data && ( + + { + setView('MAIN'); + navigate(`/home/${postId}`); + }} + > + + {data.images.length && ( + + + + )} + + + {data.content} + + + + + {deleteModal && ( + { + setDeleteModal(false); + }} + onClickActionButton={handleDelete} + /> + )} + + ) + ); +} + +const PostModalLayout = styled(Modal)` + transform: translate(-10%, -50%); +`; + +const Container = styled.div` + height: 50vh; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background-color: ${({ theme }) => theme.colors.text.primary}; + border-radius: 8px; + } + + &::-webkit-scrollbar-thumb { + background-color: ${({ theme }) => theme.colors.text.third}; + border-radius: 4px; + } +`; + +const TextContainer = styled.div` + width: 40vw; + ${({ theme: { colors } }) => ({ + color: colors.text.secondary, + })} +`; + +const ImageContainer = styled.div` + display: flex; + justify-content: center; + margin-bottom: 26px; + width: 100%; + height: 100%; +`; diff --git a/packages/client/src/features/writingModal/api/sendPost.ts b/packages/client/src/features/writingModal/api/sendPost.ts new file mode 100644 index 0000000..78ab025 --- /dev/null +++ b/packages/client/src/features/writingModal/api/sendPost.ts @@ -0,0 +1,34 @@ +import instance from 'shared/apis/AxiosInterceptor'; +import { BASE_URL } from '@constants'; + +const dummyStarData = { + color: 'pink', + size: 200, + position: { + x: 1000, + y: 7000, + z: -1600, + }, +}; // TODO: 별 커스텀 데이터로 변경 + +export const sendPost = async (text: string, files: FileList | null) => { + try { + const formData = new FormData(); + formData.append('title', '제목'); + formData.append('content', text); + formData.append('star', JSON.stringify(dummyStarData)); + if (files) { + for (let i = 0; i < files.length; i++) formData.append('file', files[i]); + } + + const response = await instance.post(`${BASE_URL}post`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response; + } catch (err) { + console.error(err); + } +}; diff --git a/packages/client/src/features/writingModal/ui/WritingModal.tsx b/packages/client/src/features/writingModal/ui/WritingModal.tsx index 66bacf1..054e394 100644 --- a/packages/client/src/features/writingModal/ui/WritingModal.tsx +++ b/packages/client/src/features/writingModal/ui/WritingModal.tsx @@ -3,23 +3,34 @@ import { Button, Modal } from 'shared/ui'; import TextArea from 'shared/ui/textArea/TextArea'; import { ModalPortal } from 'shared/ui'; import Images from './Images'; -import axios from 'axios'; import { useNavigate } from 'react-router-dom'; +import { sendPost } from '../api/sendPost'; export default function WritingModal() { const [text, setText] = useState(''); const [files, setFiles] = useState(null); const navigate = useNavigate(); + const handleSendPost = async () => { + const response = await sendPost(text, files); + if (response!.status === 201) { + navigate('/home'); + // TODO: home에서 별 정보 refetching + } else { + alert('글쓰기 실패'); + } + }; + return ( sendToServer(text, files)} + onClick={handleSendPost} size="m" buttonType="CTA-icon" + type="submit" > 다음 @@ -32,27 +43,3 @@ export default function WritingModal() { ); } - -const sendToServer = async (text: string, files: FileList | null) => { - try { - const formData = new FormData(); - formData.append('title', 'title'); - formData.append('text', text); - if (files) - for (let i = 0; i < files.length; i++) formData.append('image', files[i]); - - const config = { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }; - const response = await axios.post( - `http://www.별글.site/board`, - formData, - config, - ); - console.log(response); - } catch (error) { - console.log(error); - } -}; diff --git a/packages/client/src/pages/Home/index.tsx b/packages/client/src/pages/Home/index.tsx index a49d7a7..6f1f5aa 100644 --- a/packages/client/src/pages/Home/index.tsx +++ b/packages/client/src/pages/Home/index.tsx @@ -1,24 +1,43 @@ import Screen from 'widgets/screen'; import { useViewStore } from 'shared/store/useViewStore'; -import { usePostStore } from 'shared/store/usePostStore'; -import PostModal from 'features/postModal/PostModal'; -import { Outlet } from 'react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; import UnderBar from 'shared/ui/underBar/UnderBar'; import UpperBar from './ui/UpperBar'; +import Cookies from 'js-cookie'; +import { useEffect } from 'react'; +import { useLoadingStore } from 'shared/store/useLoadingStore'; +import WarpScreen from 'widgets/warpScreen/WarpScreen'; export default function Home() { const { view } = useViewStore(); - const { data } = usePostStore(); + const navigate = useNavigate(); + const { isLoading } = useLoadingStore(); + + useEffect(() => { + const accessToken = Cookies.get('accessToken'); + const refreshToken = Cookies.get('refreshToken'); + if (!accessToken && !refreshToken) { + navigate('/'); + } + }, []); return ( <> - {view === 'POST' && } - - + {isLoading && } + + {view === 'MAIN' && ( + <> + + + + )} ); } + +// TODO: 내 은하에서는 MAIN의 뒤로가기 버튼 안보이게 하기 +// TODO: 다른 사람 은하에서는 뒤로가기 버튼만 보이게 하기 ? diff --git a/packages/client/src/pages/Landing/index.tsx b/packages/client/src/pages/Landing/index.tsx index 6d898e3..084f5ed 100644 --- a/packages/client/src/pages/Landing/index.tsx +++ b/packages/client/src/pages/Landing/index.tsx @@ -1,30 +1,10 @@ import LandingScreen from 'widgets/landingScreen'; -import LoginModal from 'widgets/loginModal'; -import LogoAndStart from 'widgets/logoAndStart'; -import SignUpModal from 'widgets/signupModal/SignUpModal'; -import { useReducer, useState } from 'react'; -import NickNameSetModal from 'widgets/nickNameSetModal/NickNameSetModal'; +import { useState } from 'react'; import { useToastStore } from 'shared/store/useToastStore'; import { Toast } from 'shared/ui'; - -const pageReducer = ( - state: number, - action: { type: 'NEXT' | 'PREV' | 'SET'; pageIndex?: number }, -) => { - switch (action.type) { - case 'NEXT': - return state + 1; - case 'PREV': - return state - 1; - case 'SET': - return action.pageIndex ?? state; - default: - return state; - } -}; +import { Outlet } from 'react-router-dom'; export default function Landing() { - const [page, dispatch] = useReducer(pageReducer, 0); const [mouse, setMouse] = useState([0.5, 0.5]); const { text } = useToastStore.getState(); @@ -38,11 +18,7 @@ export default function Landing() { }} > {text && {text}} - - {page === 0 && } - {page === 1 && } - {page === 2 && } - {page === 3 && } + ); diff --git a/packages/client/src/shared/apis/AxiosInterceptor.ts b/packages/client/src/shared/apis/AxiosInterceptor.ts index 64dfc31..9a5f1d5 100644 --- a/packages/client/src/shared/apis/AxiosInterceptor.ts +++ b/packages/client/src/shared/apis/AxiosInterceptor.ts @@ -3,6 +3,7 @@ import { useEffect } from 'react'; const instance = axios.create({ baseURL: 'https://www.별글.site/api/', + withCredentials: true, }); interface Props { diff --git a/packages/client/src/shared/apis/index.ts b/packages/client/src/shared/apis/index.ts index a8cb769..aee9cff 100644 --- a/packages/client/src/shared/apis/index.ts +++ b/packages/client/src/shared/apis/index.ts @@ -1,2 +1,3 @@ export * from './AxiosInterceptor'; export * from './signUp'; +export * from './login'; diff --git a/packages/client/src/shared/apis/login.ts b/packages/client/src/shared/apis/login.ts new file mode 100644 index 0000000..cfc8647 --- /dev/null +++ b/packages/client/src/shared/apis/login.ts @@ -0,0 +1,51 @@ +import axios, { AxiosError } from 'axios'; +import { BASE_URL } from '@constants'; +import Cookies from 'js-cookie'; +import { NavigateFunction } from 'react-router-dom'; +import { useLoadingStore } from 'shared/store/useLoadingStore'; + +axios.defaults.withCredentials = true; + +export const postLogin = async ( + data: { + username: string; + password: string; + }, + setIdState: React.Dispatch>, + setPasswordState: React.Dispatch>, + navigate: NavigateFunction, +) => { + try { + const res = await axios.post(BASE_URL + 'auth/signin', data, { + withCredentials: true, + }); + + Cookies.set('userId', data.username, { path: '/', expires: 7 }); + Cookies.set('refreshToken', res.data.refreshToken, { + path: '/', + secure: true, + expires: 1, + }); + Cookies.set('accessToken', res.data.accessToken, { + path: '/', + secure: true, + expires: 1 / 24, + }); + navigate('/home'); + useLoadingStore.setState({ + isLoading: true, + }); + + setTimeout(() => { + useLoadingStore.setState({ + isLoading: false, + }); + }, 5500); + } catch (err) { + if (err instanceof AxiosError) { + if (err.response?.status === 404) setIdState(false); + else if (err.response?.status === 401) setPasswordState(false); + else alert(err); + } else alert(err); + } +}; diff --git a/packages/client/src/shared/hooks/useFetch.ts b/packages/client/src/shared/hooks/useFetch.ts index b1a4a29..5c81598 100644 --- a/packages/client/src/shared/hooks/useFetch.ts +++ b/packages/client/src/shared/hooks/useFetch.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react'; import axios, { AxiosError } from 'axios'; - -const BASE_URL = 'wow'; // TODO: Add base url +import { BASE_URL } from '@constants'; export const useFetch = (api: string) => { const [data, setData] = useState(); @@ -11,7 +10,9 @@ export const useFetch = (api: string) => { useEffect(() => { const fetchData = async () => { try { - const res = await axios.get(BASE_URL + api); + const res = await axios.get(BASE_URL + api, { + withCredentials: true, + }); setData(res.data); setLoading(false); } catch (err) { diff --git a/packages/client/src/shared/lib/constants/width.ts b/packages/client/src/shared/lib/constants/width.ts index 175e198..b6a62d3 100644 --- a/packages/client/src/shared/lib/constants/width.ts +++ b/packages/client/src/shared/lib/constants/width.ts @@ -1,2 +1,2 @@ export const MAX_WIDTH1 = 1210; -export const MAX_WIDTH2 = 1030; +export const MAX_WIDTH2 = 930; diff --git a/packages/client/src/shared/lib/types/post.ts b/packages/client/src/shared/lib/types/post.ts index 36c7c8b..d10106a 100644 --- a/packages/client/src/shared/lib/types/post.ts +++ b/packages/client/src/shared/lib/types/post.ts @@ -1,8 +1,6 @@ export interface PostData { + id: number; title: string; - position: THREE.Vector3; - size: number; - color: string; content: string; images: string[]; } diff --git a/packages/client/src/shared/lib/types/star.ts b/packages/client/src/shared/lib/types/star.ts new file mode 100644 index 0000000..76883d4 --- /dev/null +++ b/packages/client/src/shared/lib/types/star.ts @@ -0,0 +1,7 @@ +export interface StarData { + id: number; + title: string; + position: THREE.Vector3; + size: number; + color: string; +} diff --git a/packages/client/src/shared/store/index.ts b/packages/client/src/shared/store/index.ts new file mode 100644 index 0000000..e1d5870 --- /dev/null +++ b/packages/client/src/shared/store/index.ts @@ -0,0 +1,4 @@ +export * from './useCameraStore'; +export * from './useSignUpStore'; +export * from './useToastStore'; +export * from './useViewStore'; diff --git a/packages/client/src/shared/store/useLoadingStore.ts b/packages/client/src/shared/store/useLoadingStore.ts new file mode 100644 index 0000000..e4c9959 --- /dev/null +++ b/packages/client/src/shared/store/useLoadingStore.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +interface LoadingState { + isLoading: boolean; + setIsLoading: (id: boolean) => void; +} + +export const useLoadingStore = create((set) => ({ + isLoading: false, + setIsLoading: (value) => { + set((state) => ({ ...state, isLoading: value })); + }, +})); diff --git a/packages/client/src/shared/store/usePostStore.ts b/packages/client/src/shared/store/usePostStore.ts deleted file mode 100644 index df4b148..0000000 --- a/packages/client/src/shared/store/usePostStore.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PostData } from './../lib/types/post'; -import { create } from 'zustand'; -import * as THREE from 'three'; - -interface PostState { - data: PostData; - setData: (data: PostData) => void; -} - -export const usePostStore = create((set) => ({ - data: { - title: '', - content: '', - position: new THREE.Vector3(0, 0, 0), - size: 1, - color: '#000000', - images: [], - }, - setData: (data: PostData) => set({ data }), -})); diff --git a/packages/client/src/shared/store/useViewStore.ts b/packages/client/src/shared/store/useViewStore.ts index 798ff72..8e4b06d 100644 --- a/packages/client/src/shared/store/useViewStore.ts +++ b/packages/client/src/shared/store/useViewStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -type view = 'MAIN' | 'DETAIL' | 'WRITING' | 'POST'; +type view = 'MAIN' | 'DETAIL' | 'POST'; interface ViewState { view: view; diff --git a/packages/client/src/shared/store/userLoginStore.ts b/packages/client/src/shared/store/userLoginStore.ts deleted file mode 100644 index 8868b57..0000000 --- a/packages/client/src/shared/store/userLoginStore.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from 'zustand'; - -interface LoginState { - id: string; - setId: (id: string) => void; - password: string; - setPassword: (password: string) => void; -} - -export const useLoginStore = create()((set) => ({ - id: '', - setId: (id: string) => set({ id }), - password: '', - setPassword: (password: string) => set({ password }), -})); diff --git a/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx b/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx new file mode 100644 index 0000000..9a8a096 --- /dev/null +++ b/packages/client/src/shared/ui/alertDialog/AlertDialog.tsx @@ -0,0 +1,101 @@ +import styled from '@emotion/styled'; +import { Body02ME, Title01 } from '../styles'; +import { css } from '@emotion/react'; +import { Button } from '..'; + +interface PropsTypes extends React.HTMLAttributes { + title: string; + cancelButtonText: string; + actionButtonText: string; + onClickCancelButton: () => void; + onClickActionButton: () => void; + description?: string; +} + +export default function AlertDialog({ + title, + description, + cancelButtonText, + actionButtonText, + onClickCancelButton, + onClickActionButton, + ...args +}: PropsTypes) { + return ( + + + {title} + {description && {description}} + + + + + + + + ); +} + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 998; + background-color: rgba(0, 0, 0, 0.5); +`; + +const Layout = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 999; + + display: flex; + flex-direction: column; + width: 516px; + border-radius: 16px; + padding: 32px; + margin: 12px 0 0 0; + + ${({ theme: { colors } }) => css` + background-color: ${colors.background.bdp01_80}; + border: 2px solid ${colors.stroke.focus_80}; + `}; +`; + +const Title = styled.h1` + display: flex; + justify-content: flex-start; + margin: 0 0 8px 0; + color: ${({ theme: { colors } }) => colors.text.primary}; + ${Title01} +`; + +const Description = styled.p` + color: ${({ theme: { colors } }) => colors.text.third}; + ${Body02ME} +`; + +const ButtonsContainer = styled.div` + display: flex; + justify-content: flex-end; + margin: 32px 0 0 0; + gap: 8px; +`; diff --git a/packages/client/src/shared/ui/buttons/Button.tsx b/packages/client/src/shared/ui/buttons/Button.tsx index 26b2e08..d9fd508 100644 --- a/packages/client/src/shared/ui/buttons/Button.tsx +++ b/packages/client/src/shared/ui/buttons/Button.tsx @@ -61,7 +61,7 @@ const CustomButton = styled.button` } &:disabled { - border-color: none; + border-color: transparent; background: ${colors.primary.disabled}; color: ${colors.text.disabled}; } @@ -80,7 +80,7 @@ const CustomButton = styled.button` case 'warning-border': return css` border: 1px solid ${colors.warning.filled}; - background: none; + background: transparent; color: ${colors.warning.filled}; ${size === 'm' ? Body02ME : Body03ME} diff --git a/packages/client/src/shared/ui/index.ts b/packages/client/src/shared/ui/index.ts index 47333b7..c2e3056 100644 --- a/packages/client/src/shared/ui/index.ts +++ b/packages/client/src/shared/ui/index.ts @@ -1,6 +1,6 @@ export * from './buttons'; export * from './inputBar'; -export * from './modals'; +export * from './modal'; export * from './search'; export * from './textArea'; export * from './toast'; diff --git a/packages/client/src/shared/ui/modals/Modal.tsx b/packages/client/src/shared/ui/modal/Modal.tsx similarity index 69% rename from packages/client/src/shared/ui/modals/Modal.tsx rename to packages/client/src/shared/ui/modal/Modal.tsx index b3d012c..0a9601c 100644 --- a/packages/client/src/shared/ui/modals/Modal.tsx +++ b/packages/client/src/shared/ui/modal/Modal.tsx @@ -29,36 +29,47 @@ export default function Modal({ const isButtonExist = leftButton || rightButton; return ( - - {onClickGoBack && ( - - 뒤로가기 버튼 - - )} - - - - - {title} - {topButton} - - - {description && {description}} - - - {children} - - {isButtonExist && ( - -
{leftButton}
-
{rightButton}
-
+ + + {onClickGoBack && ( + + 뒤로가기 버튼 + )} -
-
+ + + + + {title} + {topButton} + + + {description && {description}} + + + {children} + + {isButtonExist && ( + +
{leftButton}
+
{rightButton}
+
+ )} +
+ + ); } +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 998; +`; + const Layout = styled.div` position: absolute; top: 50%; diff --git a/packages/client/src/shared/ui/modals/ModalPortal.tsx b/packages/client/src/shared/ui/modal/ModalPortal.tsx similarity index 100% rename from packages/client/src/shared/ui/modals/ModalPortal.tsx rename to packages/client/src/shared/ui/modal/ModalPortal.tsx diff --git a/packages/client/src/shared/ui/modals/index.ts b/packages/client/src/shared/ui/modal/index.ts similarity index 100% rename from packages/client/src/shared/ui/modals/index.ts rename to packages/client/src/shared/ui/modal/index.ts diff --git a/packages/client/src/shared/ui/search/Search.tsx b/packages/client/src/shared/ui/search/Search.tsx index 38fdc88..74adb35 100644 --- a/packages/client/src/shared/ui/search/Search.tsx +++ b/packages/client/src/shared/ui/search/Search.tsx @@ -34,11 +34,17 @@ export default function Search({ /> {inputState ? ( - ) : ( - )} diff --git a/packages/client/src/shared/ui/underBar/UnderBar.tsx b/packages/client/src/shared/ui/underBar/UnderBar.tsx index 8b3e1fb..432ac8f 100644 --- a/packages/client/src/shared/ui/underBar/UnderBar.tsx +++ b/packages/client/src/shared/ui/underBar/UnderBar.tsx @@ -1,37 +1,60 @@ import styled from '@emotion/styled'; import { Button } from 'shared/ui'; -import { css } from '@emotion/react'; import { Title01 } from '../styles'; import PlanetEditIcon from '@icons/icon-planetedit-24-white.svg'; import AddIcon from '@icons/icon-add-24-white.svg'; import WriteIcon from '@icons/icon-writte-24-white.svg'; -import { MAX_WIDTH1, MAX_WIDTH2 } from 'shared/lib/constants'; +import { BASE_URL, MAX_WIDTH1, MAX_WIDTH2 } from 'shared/lib/constants'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; export default function UnderBar() { const tempName = '도라에몽도라에몽도라'; + const navigate = useNavigate(); return ( {tempName}님의 은하 - - 로그아웃 - + + + + - - 우주 수정하기 - 우주 수정하기 - - - 별 스킨 만들기별 스킨 만들기 - - - 글쓰기 - 글쓰기 - + + + 우주 수정하기 + 우주 수정하기 + + + 별 스킨 만들기별 스킨 만들기 + + navigate('/home/writing')} + > + 글쓰기 + 글쓰기 + + ); @@ -59,44 +82,36 @@ const Layout = styled.div` } @media (max-width: ${MAX_WIDTH2}px) { - width: 1000px; + width: 900px; } `; -const Name = styled.p` - color: ${({ theme }) => theme.colors.text.primary}; - ${Title01} -`; - const ButtonsContainer = styled.div` display: flex; `; -const ButtonBasicStyle = css` - width: 155px; - height: 70px; -`; - -const LogoutButton = styled(Button)` - height: 70px; - margin: 0 24px 0 0; +const SmallButtonsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 4px; `; -const SpaceEditButton = styled(Button)` - ${ButtonBasicStyle} - margin: 0 8px 0 0; +const BigButtonsContainer = styled.div` + display: flex; + gap: 8px; `; -const SkinCreateButton = styled(Button)` - ${ButtonBasicStyle} - margin: 0 16px 0 0; +const BigButton = styled(Button)` + width: 150px; + height: 76px; `; -const WritingButton = styled(Button)` - ${ButtonBasicStyle} +const Name = styled.p` + color: ${({ theme }) => theme.colors.text.primary}; + ${Title01} `; const Line = styled.div` - margin: 0 24px 0 0; + margin: 0 18px; border-left: 1px solid ${({ theme }) => theme.colors.stroke.sc}; `; diff --git a/packages/client/src/shared/utils/index.ts b/packages/client/src/shared/utils/index.ts new file mode 100644 index 0000000..387052c --- /dev/null +++ b/packages/client/src/shared/utils/index.ts @@ -0,0 +1 @@ +export * from './random'; diff --git a/packages/client/src/widgets/galaxy/Galaxy.tsx b/packages/client/src/widgets/galaxy/Galaxy.tsx index bba94ab..376d383 100644 --- a/packages/client/src/widgets/galaxy/Galaxy.tsx +++ b/packages/client/src/widgets/galaxy/Galaxy.tsx @@ -15,7 +15,9 @@ export default function Galaxy() { const count = STARS_NUM * starTypes.percentage[i]; const size = starTypes.size[i]; const color = starTypes.color[i]; - starList.push(); + starList.push( + , + ); } return starList; }, []); diff --git a/packages/client/src/widgets/galaxy/lib/constants/index.ts b/packages/client/src/widgets/galaxy/lib/constants/index.ts index 0731a7d..c3b2952 100644 --- a/packages/client/src/widgets/galaxy/lib/constants/index.ts +++ b/packages/client/src/widgets/galaxy/lib/constants/index.ts @@ -14,3 +14,9 @@ export const starTypes = { color: [0xffcc6f, 0xffd2a1, 0xfff4ea, 0xf8f7ff, 0xcad7ff, 0xaabfff], size: [0.7, 0.7, 1.15, 1.48, 2.0, 2.5, 3.5], }; + +// export const ARMS_X_DIST = 5000; +// export const ARMS_Z_DIST = 1000; +// export const GALAXY_THICKNESS = 300; +// export const SPIRAL = 1.2; +// export const SPIRAL_START = 1000; diff --git a/packages/client/src/widgets/galaxy/lib/modules/instances.tsx b/packages/client/src/widgets/galaxy/lib/modules/instances.tsx index f6d6efe..729b5c8 100644 --- a/packages/client/src/widgets/galaxy/lib/modules/instances.tsx +++ b/packages/client/src/widgets/galaxy/lib/modules/instances.tsx @@ -28,14 +28,14 @@ export default function Instances({ count, size, color }: PropsType) { for (let arm = 0; arm < ARMS; arm++) { for (let star = 0; star < arms / (ARMS + 1); star++) { tempObject.position.copy( - getSpiralPositions((arm * 2 * Math.PI) / ARMS), + getSpiralPositions(((2 * Math.PI) / ARMS) * arm), ); tempObject.updateMatrix(); instancedMeshRef.current.setMatrixAt(index++, tempObject.matrix); } } for (let i = 0; i < center; i++) { - tempObject.position.copy(getSpherePositions(2000)); + tempObject.position.copy(getSpherePositions()); tempObject.updateMatrix(); instancedMeshRef.current.setMatrixAt(index++, tempObject.matrix); } diff --git a/packages/client/src/widgets/galaxy/lib/modules/positions.ts b/packages/client/src/widgets/galaxy/lib/modules/positions.ts index 24cc5a4..ef7a0a2 100644 --- a/packages/client/src/widgets/galaxy/lib/modules/positions.ts +++ b/packages/client/src/widgets/galaxy/lib/modules/positions.ts @@ -22,14 +22,27 @@ export const getSpiralPositions = (offset: number) => { return new THREE.Vector3(r * Math.cos(theta), y, r * Math.sin(theta)); }; -export const getSpherePositions = (radius: number) => { +// export const getSpiralPositions = () => { +// const x = getGaussianRandomFloat(0, ARMS_X_DIST); +// const y = getGaussianRandomFloat(0, GALAXY_THICKNESS); +// const z = getGaussianRandomFloat(0, ARMS_Z_DIST); +// const r = Math.sqrt(x ** 2 + z ** 2); +// if (r < SPIRAL_START) return new THREE.Vector3(x, y, z); + +// const theta = Math.log(r / SPIRAL_START) * SPIRAL * -Math.PI; +// const yAxis = new THREE.Vector3(0, 1, 0); + +// return new THREE.Vector3(x, y, z).applyAxisAngle(yAxis, theta); +// }; + +export const getSpherePositions = () => { const x = getRandomFloat(0, Math.PI * 2); const y = getGaussianRandomFloat(0, Math.PI / 5); - const r = getGaussianRandomFloat(0, radius); + const r = getGaussianRandomFloat(0, 1); return new THREE.Vector3( - ((r * Math.sin(x) * Math.cos(y)) / radius) * GALAXY_THICKNESS * 5, - ((r * Math.sin(y)) / radius) * GALAXY_THICKNESS * 4, - ((r * Math.cos(x) * Math.cos(y)) / radius) * GALAXY_THICKNESS * 5, + r * Math.sin(x) * Math.cos(y) * ARMS_X_DIST, + r * Math.sin(y) * GALAXY_THICKNESS * 4, + r * Math.cos(x) * Math.cos(y) * ARMS_X_DIST, ); }; diff --git a/packages/client/src/widgets/loginModal/index.tsx b/packages/client/src/widgets/loginModal/index.tsx index 5428470..a933789 100644 --- a/packages/client/src/widgets/loginModal/index.tsx +++ b/packages/client/src/widgets/loginModal/index.tsx @@ -1,29 +1,37 @@ import { Modal } from 'shared/ui'; import { TopButton, LeftButton, RightButton, LoginContent } from './ui'; -import { useLoginStore } from 'shared/store/userLoginStore'; -import axios from 'axios'; import { useNavigate } from 'react-router-dom'; -import { BASE_URL } from '@constants'; -interface PropsType { - changePage: React.Dispatch<{ type: 'NEXT' | 'PREV' }>; -} +import Cookies from 'js-cookie'; +import { useState, useEffect } from 'react'; +import { postLogin } from 'shared/apis'; -export default function LoginModal({ changePage }: PropsType) { - const { id, password, setPassword } = useLoginStore(); +export default function LoginModal() { + const [id, setId] = useState(Cookies.get('userId') ?? ''); + const [idState, setIdState] = useState(true); + const [password, setPassword] = useState(''); + const [passwordState, setPasswordState] = useState(true); const navigate = useNavigate(); + const isValid = () => { + return id.length && password.length && idState && passwordState; + }; + const handleLoginSubmit = () => { - if (id.length && password.length === 0) return; + if (!isValid()) return; const data = { username: id, password: password, }; setPassword(''); - axios.post(BASE_URL + 'auth/signin', data).then((res) => { - if (res.status === 200) navigate('/home'); - else console.log(res.status); - }); + postLogin(data, setIdState, setPasswordState, navigate); }; + + useEffect(() => { + const accessToken = Cookies.get('accessToken'); + const refreshToken = Cookies.get('refreshToken'); + if (refreshToken || accessToken) navigate('/home'); + }); + return (
{ @@ -33,13 +41,18 @@ export default function LoginModal({ changePage }: PropsType) { > changePage({ type: 'PREV' })} />} - rightButton={} - leftButton={ changePage({ type: 'NEXT' })} />} - onClickGoBack={() => changePage({ type: 'PREV' })} + topButton={ navigate('/')} />} + rightButton={} + leftButton={ navigate('/signup')} />} + onClickGoBack={() => navigate('/')} style={{ width: '516px' }} > - +
); diff --git a/packages/client/src/widgets/loginModal/ui/LoginContent.tsx b/packages/client/src/widgets/loginModal/ui/LoginContent.tsx index c00500d..e7aa742 100644 --- a/packages/client/src/widgets/loginModal/ui/LoginContent.tsx +++ b/packages/client/src/widgets/loginModal/ui/LoginContent.tsx @@ -1,33 +1,83 @@ import { Input } from 'shared/ui'; import styled from '@emotion/styled'; -import { useLoginStore } from 'shared/store/userLoginStore'; +import { css } from '@emotion/react'; +import { Caption } from 'shared/ui/styles'; -export default function LoginContent() { - const { id, setId, password, setPassword } = useLoginStore(); +interface PropsType { + useId: [string, React.Dispatch>]; + useIdState: [boolean, React.Dispatch>]; + usePassword: [string, React.Dispatch>]; + usePasswordState: [boolean, React.Dispatch>]; +} +export default function LoginContent({ + useId: [id, setId], + useIdState: [idState, setIdState], + usePassword: [password, setPassword], + usePasswordState: [passwordState, setPasswordState], +}: PropsType) { return ( - setId(e.target.value)} - autoComplete="off" - value={id} - /> - setPassword(e.target.value)} - value={password} - /> + + { + if (idState === false) setIdState(true); + setId(e.target.value); + }} + autoComplete="off" + value={id} + /> +

등록되지 않은 계정입니다

+
+ + { + if (passwordState === false) setPasswordState(true); + setPassword(e.target.value); + }} + value={password} + /> +

비밀번호가 일치하지 않습니다

+
); } +interface LoginInputProps { + state: boolean; +} + +const LoginInput = styled.div` + p { + position: absolute; + display: none; + margin-top: 4px; + ${Caption} + } + + ${({ state, theme: { colors } }) => { + if (state === false) { + return css` + Input { + border-color: ${colors.warning.filled}; + } + p { + display: flex; + color: ${colors.warning.filled}; + } + `; + } + }}; +`; + const Container = styled.div` display: flex; flex-direction: column; diff --git a/packages/client/src/widgets/loginModal/ui/RightButton.tsx b/packages/client/src/widgets/loginModal/ui/RightButton.tsx index 1e9109b..d0c406a 100644 --- a/packages/client/src/widgets/loginModal/ui/RightButton.tsx +++ b/packages/client/src/widgets/loginModal/ui/RightButton.tsx @@ -1,16 +1,12 @@ import { Button } from 'shared/ui'; -import { useLoginStore } from 'shared/store/userLoginStore'; -export default function RightButton() { - const { id, password } = useLoginStore(); +interface PropsType { + disabled: boolean; +} +export default function RightButton({ disabled }: PropsType) { return ( - ); diff --git a/packages/client/src/widgets/logoAndStart/index.tsx b/packages/client/src/widgets/logoAndStart/index.tsx index 3103ce8..59e1cd6 100644 --- a/packages/client/src/widgets/logoAndStart/index.tsx +++ b/packages/client/src/widgets/logoAndStart/index.tsx @@ -1,20 +1,19 @@ import { Button } from 'shared/ui'; import styled from '@emotion/styled'; -import React from 'react'; import { Title02 } from 'shared/ui/styles'; +import { useNavigate } from 'react-router-dom'; -interface PropsType { - changePage: React.Dispatch<{ type: 'NEXT' | 'PREV' }>; -} +export default function LogoAndStart() { + const navigate = useNavigate(); -export default function LogoAndStart({ changePage }: PropsType) { return ( Logo