diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9bd4613d..91a6b2ef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: S3 deploy on: push: - branches: ['Master'] + branches: ['Weekly'] env: AWS_REGION: ap-northeast-2 S3_BUCKET_NAME: sinitto diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index a68c45f6..a0c3e00e 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -1,8 +1,11 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { RouterPath } from './path'; -import MainPage from '@/pages/common/main'; -import RegisterPage from '@/pages/common/register'; +import MainPage from '@/pages/common/main/MainPage'; +import { RedirectPage } from '@/pages/common/redirect'; +import RegisterPage from '@/pages/common/register/RegisterPage'; +import GuideLinePage from '@/pages/guard/guide-line'; +import GuardMyPage from '@/pages/guard/mypage'; import SeniorRegisterPage from '@/pages/guard/register'; import SinittoReviewPage from '@/pages/guard/review'; import ServiceHistoryPage from '@/pages/guard/service-history'; @@ -12,7 +15,8 @@ import SinittoGuideLinePage from '@/pages/sinitto/guide-line'; import HelloCallListPage from '@/pages/sinitto/hello-call/hello-call-list'; import HelloCallReportPage from '@/pages/sinitto/hello-call/hello-call-report'; import HelloCallServicePage from '@/pages/sinitto/hello-call/hello-call-service'; -import MyPage from '@/pages/sinitto/mypage'; +import SinittoMypage from '@/pages/sinitto/mypage'; +import { Layout } from '@/shared/components'; const router = createBrowserRouter([ { @@ -20,64 +24,52 @@ const router = createBrowserRouter([ element: , }, { - path: RouterPath.REGISTER, - element: , - }, - { - path: RouterPath.MYPAGE, - element: , - }, - { - path: RouterPath.SERVICE_HISTORY, - element: , - }, - { - path: RouterPath.HELLO_CALL, - children: [ - { - index: true, - element: , - }, - { - path: RouterPath.HELLO_CALL_SERVICE, - element: , - }, - { - path: RouterPath.HELLO_CALL_REPORT, - element: , - }, - ], - }, - { - path: RouterPath.SENIOR_REGISTER, - element: , - }, - { - path: RouterPath.CALL_BACK_LIST, + path: RouterPath.ROOT, + element: , children: [ + { path: RouterPath.SIGNUP, element: }, + { path: RouterPath.REDIRECT, element: }, + { path: RouterPath.SINITTO_MYPAGE, element: }, + { path: RouterPath.GUARD_MYPAGE, element: }, + { path: RouterPath.GUARD_GUIDELINE, element: }, + { path: RouterPath.SERVICE_HISTORY, element: }, { - index: true, - element: , - }, - { - path: RouterPath.CALL_BACK_DETAIL, + path: RouterPath.HELLO_CALL, children: [ + { index: true, element: }, { - index: true, - element: , + path: RouterPath.HELLO_CALL_SERVICE, + element: , }, { - path: RouterPath.CALL_BACK_GUID_LINE, - element: , + path: RouterPath.HELLO_CALL_REPORT, + element: , }, ], }, + { path: RouterPath.SENIOR_REGISTER, element: }, + { + path: RouterPath.CALL_BACK_LIST, + children: [ + { index: true, element: }, + { + path: RouterPath.CALL_BACK_DETAIL, + children: [ + { index: true, element: }, + { + path: RouterPath.CALL_BACK_GUID_LINE, + element: , + }, + ], + }, + ], + }, + { + path: RouterPath.SINITTO_REVIEW, + element: , + }, ], }, - { - path: RouterPath.SINITTO_REVIEW, - element: , - }, ]); export const Routes = () => { diff --git a/src/app/routes/path.ts b/src/app/routes/path.ts index 741f6f7e..0296f392 100644 --- a/src/app/routes/path.ts +++ b/src/app/routes/path.ts @@ -1,8 +1,12 @@ export const RouterPath = { ROOT: '/', LOGIN: '/login', + SIGNUP: '/signup', REGISTER: '/register', - MYPAGE: `/mypage`, + REDIRECT: '/redirection', + GUARD_MYPAGE: `/guard/mypage`, + GUARD_GUIDELINE: `/guard/guideline`, + SINITTO_MYPAGE: `/sinitto/mypage`, SERVICE_HISTORY: `/service-history`, HELLO_CALL_SERVICE: 'service', HELLO_CALL_REPORT: 'report', diff --git a/src/pages/common/main/index.tsx b/src/pages/common/main/MainPage.tsx similarity index 100% rename from src/pages/common/main/index.tsx rename to src/pages/common/main/MainPage.tsx diff --git a/src/pages/common/main/components/login-button/index.tsx b/src/pages/common/main/components/login-button/index.tsx index dbb3aa94..9f63c3dd 100644 --- a/src/pages/common/main/components/login-button/index.tsx +++ b/src/pages/common/main/components/login-button/index.tsx @@ -1,13 +1,18 @@ +import { Link } from 'react-router-dom'; + import Logo from '../../assets/kakao.svg'; +import { KAKAO_AUTH_URL } from '@/shared/constants'; import { Image, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; const LoginButton = () => { return ( - - kakao-icon - 카카오톡 로그인 - + + + kakao-icon + 카카오톡 로그인 + + ); }; diff --git a/src/pages/common/redirect/RedirectPage.tsx b/src/pages/common/redirect/RedirectPage.tsx new file mode 100644 index 00000000..d793cd3d --- /dev/null +++ b/src/pages/common/redirect/RedirectPage.tsx @@ -0,0 +1,16 @@ +import { useLocation } from 'react-router-dom'; + +import { RedirectSection } from './components'; + +const RedirectPage = () => { + const location = useLocation(); + + const code = new URLSearchParams(location.search).get('code'); + if (!code) { + return
로그인을 다시 진행해주세요.
; + } + + return ; +}; + +export default RedirectPage; diff --git a/src/pages/common/redirect/components/index.ts b/src/pages/common/redirect/components/index.ts new file mode 100644 index 00000000..c0b93c2e --- /dev/null +++ b/src/pages/common/redirect/components/index.ts @@ -0,0 +1 @@ +export * from './redirect-section'; diff --git a/src/pages/common/redirect/components/redirect-section/RedirectSection.tsx b/src/pages/common/redirect/components/redirect-section/RedirectSection.tsx new file mode 100644 index 00000000..c89a40db --- /dev/null +++ b/src/pages/common/redirect/components/redirect-section/RedirectSection.tsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useGetKakaoCallback } from '../../store/hooks'; +import { Flex, Spinner, Text } from '@chakra-ui/react'; + +type Props = { + code: string; +}; + +const RedirectSection = ({ code }: Props) => { + const navigate = useNavigate(); + + const { data } = useGetKakaoCallback(code); + + useEffect(() => { + if (data) { + const accessToken = data.accessToken; + const refreshToken = data.refreshToken; + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', refreshToken); + + navigate('/'); + } + }, [data, navigate]); + + return ( + + + 로그인 진행중! + + ); +}; + +export default RedirectSection; diff --git a/src/pages/common/redirect/components/redirect-section/index.ts b/src/pages/common/redirect/components/redirect-section/index.ts new file mode 100644 index 00000000..b071f5e9 --- /dev/null +++ b/src/pages/common/redirect/components/redirect-section/index.ts @@ -0,0 +1 @@ +export { default as RedirectSection } from './RedirectSection'; diff --git a/src/pages/common/redirect/index.ts b/src/pages/common/redirect/index.ts new file mode 100644 index 00000000..a785eba6 --- /dev/null +++ b/src/pages/common/redirect/index.ts @@ -0,0 +1 @@ +export { default as RedirectPage } from './RedirectPage'; diff --git a/src/pages/common/redirect/store/api/index.ts b/src/pages/common/redirect/store/api/index.ts new file mode 100644 index 00000000..9a36a5d3 --- /dev/null +++ b/src/pages/common/redirect/store/api/index.ts @@ -0,0 +1 @@ +export { getKakaoCallback } from './kakao-callback.api'; diff --git a/src/pages/common/redirect/store/api/kakao-callback.api.ts b/src/pages/common/redirect/store/api/kakao-callback.api.ts new file mode 100644 index 00000000..2863822e --- /dev/null +++ b/src/pages/common/redirect/store/api/kakao-callback.api.ts @@ -0,0 +1,26 @@ +import { fetchInstance } from '@/shared/api/instance'; + +export type KakaoCallbackResponse = { + accessToken: string; + refreshToken: string; + redirectUrl: string; + email: string; + isSinitto: boolean; + isMember: boolean; +}; + +const getKakaoCallbackPath = () => '/api/auth/oauth/kakao/callback'; + +export const KakaoCallbackQueryKey = [getKakaoCallbackPath()]; + +export const getKakaoCallback = async ( + code: string +): Promise => { + const response = await fetchInstance.get(getKakaoCallbackPath(), { + params: { + code, + }, + }); + console.log(response.data); + return response.data; +}; diff --git a/src/pages/common/redirect/store/hooks/index.ts b/src/pages/common/redirect/store/hooks/index.ts new file mode 100644 index 00000000..63f73f69 --- /dev/null +++ b/src/pages/common/redirect/store/hooks/index.ts @@ -0,0 +1 @@ +export { useGetKakaoCallback } from './useGetKakaoCallback'; diff --git a/src/pages/common/redirect/store/hooks/useGetKakaoCallback.ts b/src/pages/common/redirect/store/hooks/useGetKakaoCallback.ts new file mode 100644 index 00000000..e6bcae58 --- /dev/null +++ b/src/pages/common/redirect/store/hooks/useGetKakaoCallback.ts @@ -0,0 +1,13 @@ +import { + getKakaoCallback, + KakaoCallbackQueryKey, + KakaoCallbackResponse, +} from '../api/kakao-callback.api'; +import { useQuery } from '@tanstack/react-query'; + +export const useGetKakaoCallback = (code: string) => { + return useQuery({ + queryKey: [KakaoCallbackQueryKey, code], + queryFn: () => getKakaoCallback(code), + }); +}; diff --git a/src/pages/common/register/index.tsx b/src/pages/common/register/RegisterPage.tsx similarity index 65% rename from src/pages/common/register/index.tsx rename to src/pages/common/register/RegisterPage.tsx index 9677a74c..0451a505 100644 --- a/src/pages/common/register/index.tsx +++ b/src/pages/common/register/RegisterPage.tsx @@ -1,11 +1,10 @@ import { useState } from 'react'; import { useForm } from 'react-hook-form'; -import { RegisterFields } from './components/register-fields'; -import { RegisterType } from './components/register-type'; -import { Tos } from './components/tos'; +import useRegister from './api/hooks/useRegister'; +import { RegisterFields, RegisterType, Tos } from './components'; import { FormValues } from './types'; -import { BasicButton } from '@/shared/components/common/button'; +import { BasicButton } from '@/shared/components'; import { Divider } from '@chakra-ui/react'; import styled from '@emotion/styled'; @@ -17,13 +16,26 @@ const RegisterPage = () => { formState: { errors }, } = useForm(); + // 회원가입 처리 + const mutation = useRegister(); + const handleUserType = (id: string) => { setUserType(id); }; const onSubmit = (data: FormValues) => { - // 회원가입 api - console.log(userType, data); + // request 에 맞게 데이터 병합 + const isSinitto = userType === 'sinitto'; + + const requestData = { + name: data.name, + phoneNumber: data.phoneNumber, + email: 'test1@example.com', // 임시 (카카오 로그인 후 넘겨받기) + isSinitto, + }; + console.log(requestData); + // 회원가입 API 호출 + mutation.mutate(requestData); }; return ( diff --git a/src/pages/common/register/api/hooks/index.ts b/src/pages/common/register/api/hooks/index.ts new file mode 100644 index 00000000..49ce6497 --- /dev/null +++ b/src/pages/common/register/api/hooks/index.ts @@ -0,0 +1 @@ +export { default as useRegister } from './useRegister'; diff --git a/src/pages/common/register/api/hooks/useRegister.ts b/src/pages/common/register/api/hooks/useRegister.ts new file mode 100644 index 00000000..f5e201e8 --- /dev/null +++ b/src/pages/common/register/api/hooks/useRegister.ts @@ -0,0 +1,47 @@ +import { useNavigate } from 'react-router-dom'; + +import { AxiosError } from 'axios'; + +import { RouterPath } from '@/app/routes/path'; +import { + SignupApiResponse, + registerUser, +} from '@/shared/api/auth/user-register'; +import { authLocalStorage } from '@/shared/utils/storage'; +import { useMutation } from '@tanstack/react-query'; + +const useRegister = () => { + const navigate = useNavigate(); + + const handleSuccess = (data: SignupApiResponse) => { + if ('status' in data && data.status === 207) { + alert(data.detail); + } else { + console.log(data); + if ('accessToken' in data) { + authLocalStorage.set(data.accessToken); + authLocalStorage.set(data.refreshToken); + alert('회원가입이 완료되었습니다.'); + navigate(data.isSinitto === 'true' ? RouterPath.ROOT : RouterPath.ROOT); + } + } + }; + + const handleError = (error: AxiosError) => { + if (error.response && error.response.data) { + alert('회원가입 중 오류가 발생했습니다.'); + } else { + alert('회원가입 중 네트워크 오류가 발생했습니다.'); + } + }; + + const mutation = useMutation({ + mutationFn: registerUser, + onSuccess: handleSuccess, + onError: handleError, + }); + + return mutation; +}; + +export default useRegister; diff --git a/src/pages/common/register/api/index.ts b/src/pages/common/register/api/index.ts new file mode 100644 index 00000000..4cc90d02 --- /dev/null +++ b/src/pages/common/register/api/index.ts @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/src/pages/common/register/components/index.ts b/src/pages/common/register/components/index.ts new file mode 100644 index 00000000..4bd07f13 --- /dev/null +++ b/src/pages/common/register/components/index.ts @@ -0,0 +1,6 @@ +export * from './register-fields'; +export * from './register-fields/form-fields'; +export * from './register-type'; +export * from './register-type/type-button'; +export * from './tos'; +export * from './tos/content'; diff --git a/src/pages/common/register/components/register-fields/index.tsx b/src/pages/common/register/components/register-fields/RegisterFields.tsx similarity index 95% rename from src/pages/common/register/components/register-fields/index.tsx rename to src/pages/common/register/components/register-fields/RegisterFields.tsx index cf948735..56a27be9 100644 --- a/src/pages/common/register/components/register-fields/index.tsx +++ b/src/pages/common/register/components/register-fields/RegisterFields.tsx @@ -1,7 +1,7 @@ import { FieldErrors, UseFormRegister } from 'react-hook-form'; import type { FormValues } from '../../types'; -import { FormField } from './form-fileds'; +import { FormField } from './form-fields'; import styled from '@emotion/styled'; type Props = { diff --git a/src/pages/common/register/components/register-fields/form-fileds/index.tsx b/src/pages/common/register/components/register-fields/form-fields/FormField.tsx similarity index 100% rename from src/pages/common/register/components/register-fields/form-fileds/index.tsx rename to src/pages/common/register/components/register-fields/form-fields/FormField.tsx diff --git a/src/pages/common/register/components/register-fields/form-fields/index.ts b/src/pages/common/register/components/register-fields/form-fields/index.ts new file mode 100644 index 00000000..0e97de67 --- /dev/null +++ b/src/pages/common/register/components/register-fields/form-fields/index.ts @@ -0,0 +1 @@ +export { FormField } from './FormField'; diff --git a/src/pages/common/register/components/register-fields/index.ts b/src/pages/common/register/components/register-fields/index.ts new file mode 100644 index 00000000..bd4ac734 --- /dev/null +++ b/src/pages/common/register/components/register-fields/index.ts @@ -0,0 +1 @@ +export { RegisterFields } from './RegisterFields'; diff --git a/src/pages/common/register/components/register-type/index.tsx b/src/pages/common/register/components/register-type/RegisterType.tsx similarity index 95% rename from src/pages/common/register/components/register-type/index.tsx rename to src/pages/common/register/components/register-type/RegisterType.tsx index ece96f2c..f99d770d 100644 --- a/src/pages/common/register/components/register-type/index.tsx +++ b/src/pages/common/register/components/register-type/RegisterType.tsx @@ -1,4 +1,4 @@ -import { TypeButton } from './type-button.tsx'; +import { TypeButton } from './type-button'; import styled from '@emotion/styled'; type Props = { diff --git a/src/pages/common/register/components/register-type/index.ts b/src/pages/common/register/components/register-type/index.ts new file mode 100644 index 00000000..7894d59a --- /dev/null +++ b/src/pages/common/register/components/register-type/index.ts @@ -0,0 +1 @@ +export { RegisterType } from './RegisterType'; diff --git a/src/pages/common/register/components/register-type/type-button.tsx/index.tsx b/src/pages/common/register/components/register-type/type-button/TypeButton.tsx similarity index 100% rename from src/pages/common/register/components/register-type/type-button.tsx/index.tsx rename to src/pages/common/register/components/register-type/type-button/TypeButton.tsx diff --git a/src/pages/common/register/components/register-type/type-button/index.ts b/src/pages/common/register/components/register-type/type-button/index.ts new file mode 100644 index 00000000..8436efb3 --- /dev/null +++ b/src/pages/common/register/components/register-type/type-button/index.ts @@ -0,0 +1 @@ +export { TypeButton } from './TypeButton'; diff --git a/src/pages/common/register/components/tos/index.tsx b/src/pages/common/register/components/tos/Tos.tsx similarity index 100% rename from src/pages/common/register/components/tos/index.tsx rename to src/pages/common/register/components/tos/Tos.tsx diff --git a/src/pages/common/register/components/tos/content/index.tsx b/src/pages/common/register/components/tos/content/TosContent.tsx similarity index 100% rename from src/pages/common/register/components/tos/content/index.tsx rename to src/pages/common/register/components/tos/content/TosContent.tsx diff --git a/src/pages/common/register/components/tos/content/index.ts b/src/pages/common/register/components/tos/content/index.ts new file mode 100644 index 00000000..3915821b --- /dev/null +++ b/src/pages/common/register/components/tos/content/index.ts @@ -0,0 +1 @@ +export { TosContent } from './TosContent'; diff --git a/src/pages/common/register/components/tos/index.ts b/src/pages/common/register/components/tos/index.ts new file mode 100644 index 00000000..132d4984 --- /dev/null +++ b/src/pages/common/register/components/tos/index.ts @@ -0,0 +1 @@ +export { Tos } from './Tos'; diff --git a/src/pages/guard/guide-line/.gitkeep b/src/pages/guard/guide-line/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/guard/guide-line/components/guide-info-box/index.tsx b/src/pages/guard/guide-line/components/guide-info-box/index.tsx new file mode 100644 index 00000000..6a05ae6d --- /dev/null +++ b/src/pages/guard/guide-line/components/guide-info-box/index.tsx @@ -0,0 +1,49 @@ +import { Box, Flex, Text } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +type GuidelineType = { + title: string; + content: string; +}; + +const GuideLineInfo = ({ guideline }: { guideline: GuidelineType }) => { + return ( + + + + {guideline.title} + + + {guideline.content} + + + + ); +}; + +export default GuideLineInfo; + +const GuideLineInfoContainer = styled(Flex)` + width: 100%; + max-width: 330px; + min-height: 8rem; + background-color: var(--color-white); + border: 1px solid var(--color-white); + border-radius: 10px; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + margin: 0.5rem 0; + padding: 1rem; +`; + +const InfoText = styled(Text)` + font-size: 0.8rem; + color: var(--color-black); +`; + +const InfoBox = styled(Box)` + width: 100%; + max-width: 300px; + display: flex; + flex-direction: row; + justify-content: space-between; +`; diff --git a/src/pages/guard/guide-line/data/index.tsx b/src/pages/guard/guide-line/data/index.tsx new file mode 100644 index 00000000..01108e2e --- /dev/null +++ b/src/pages/guard/guide-line/data/index.tsx @@ -0,0 +1,39 @@ +const GUIDELINE_DATA = [ + { + id: 1, + title: '산격초등학교', + content: '산격초등학교에 자주 내리시는 데 집은 어디어디입니다.', + }, + { + id: 2, + title: '경로당', + content: + '수지고등학교 회전 교차로 오른쪽 작은 길로 진입하여 우회전 후 마지막에 위치한 단독 건물입니다!', + }, + { + id: 3, + title: '경로당', + content: + '수지고등학교 회전 교차로 오른쪽 작은 길로 진입하여 우회전 후 마지막에 위치한 단독 건물입니다!', + }, + { + id: 4, + title: '경로당', + content: + '수지고등학교 회전 교차로 오른쪽 작은 길로 진입하여 우회전 후 마지막에 위치한 단독 건물입니다!', + }, + { + id: 5, + title: '경로당', + content: + '수지고등학교 회전 교차로 오른쪽 작은 길로 진입하여 우회전 후 마지막에 위치한 단독 건물입니다!', + }, + { + id: 6, + title: '경로당', + content: + '수지고등학교 회전 교차로 오른쪽 작은 길로 진입하여 우회전 후 마지막에 위치한 단독 건물입니다!', + }, +]; + +export default GUIDELINE_DATA; diff --git a/src/pages/guard/guide-line/index.tsx b/src/pages/guard/guide-line/index.tsx new file mode 100644 index 00000000..8b492c5a --- /dev/null +++ b/src/pages/guard/guide-line/index.tsx @@ -0,0 +1,105 @@ +import GuideLineInfo from './components/guide-info-box'; +import GUIDELINE_DATA from './data'; +import { Box, Flex, Input, Text, Textarea } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +const GuideLinePage = () => { + const guidelines = GUIDELINE_DATA; + + return ( + + + {guidelines.map((guideline) => ( + + ))} + + + + 가이드라인 제목 + + + + 가이드라인 내용 + + + 가이드라인 추가하기 + + + ); +}; + +export default GuideLinePage; + +const Container = styled(Box)` + position: relative; + height: 100vh; +`; +const RegisterBox = styled(Box)` + position: absolute; + bottom: 0; + width: 100%; + height: 350px; + left: 50%; + transform: translateX(-50%); + max-width: 370px; + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--color-white-gray); + border: 1px solid var(--color-white-gray); + border-radius: 15px; +`; + +const InputBox = styled(Box)` + width: 300px; + height: 80px; + display: flex; + flex-direction: column; + margin: 0.5rem; +`; + +const GuideLineContentInput = styled(Textarea)` + background-color: var(--color-white); + border: none; + font-size: 1rem; +`; + +const InputText = styled(Text)` + font-size: 18px; + font-weight: bold; + margin-bottom: 0.5rem; +`; + +const GuideLineInput = styled(Input)` + background-color: var(--color-white); + border: none; + font-size: 1rem; +`; + +const StyledButton = styled.button` + position: absolute; + bottom: 1rem; + width: 300px; + height: 40px; + background-color: #c69090; + color: #ffffff; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s; + + &:hover { + background-color: #a67070; + } + + &:active { + transform: scale(0.98); + } +`; diff --git a/src/pages/guard/guide-line/store/api/view-specific-guideline.api.ts b/src/pages/guard/guide-line/store/api/view-specific-guideline.api.ts new file mode 100644 index 00000000..11d200e0 --- /dev/null +++ b/src/pages/guard/guide-line/store/api/view-specific-guideline.api.ts @@ -0,0 +1,27 @@ +import { fetchInstance } from '@/shared/api/instance'; + +export type ViewSpecificGuidelineResponse = { + title: string; + content: string; +}[]; + +const getViewSpecificGuidelinePath = (guidelineId: number) => + `/api/guideline/${guidelineId}`; + +export const getViewSpecificGuidelineQueryKey = (guidelineId: number) => [ + getViewSpecificGuidelinePath(guidelineId), +]; + +export const getViewSpecificGuideline = async ( + guidelineId: number +): Promise => { + const response = await fetchInstance.get( + getViewSpecificGuidelinePath(guidelineId), + { + params: { + guidelineId, + }, + } + ); + return response.data; +}; diff --git a/src/pages/guard/guide-line/store/hooks/getViewSpecificGuideline.ts b/src/pages/guard/guide-line/store/hooks/getViewSpecificGuideline.ts new file mode 100644 index 00000000..c7ebc26a --- /dev/null +++ b/src/pages/guard/guide-line/store/hooks/getViewSpecificGuideline.ts @@ -0,0 +1,13 @@ +import { + getViewSpecificGuideline, + getViewSpecificGuidelineQueryKey, + ViewSpecificGuidelineResponse, +} from '../api/view-specific-guideline.api'; +import { useQuery } from '@tanstack/react-query'; + +export const useGetViewSpecificGuideline = (guidelineId: number) => { + return useQuery({ + queryKey: [getViewSpecificGuidelineQueryKey(guidelineId)], + queryFn: () => getViewSpecificGuideline(guidelineId), + }); +}; diff --git a/src/pages/sinitto/mypage/components/ProfileBox/index.tsx b/src/pages/guard/mypage/components/profile-box/index.tsx similarity index 97% rename from src/pages/sinitto/mypage/components/ProfileBox/index.tsx rename to src/pages/guard/mypage/components/profile-box/index.tsx index 3431a621..0a838365 100644 --- a/src/pages/sinitto/mypage/components/ProfileBox/index.tsx +++ b/src/pages/guard/mypage/components/profile-box/index.tsx @@ -6,7 +6,7 @@ const ProfileBox = () => { - 홍길동씨 환영합니다. + 홍길동님 환영합니다. 서비스 이용 방법 한번에 이해하기! diff --git a/src/pages/guard/mypage/index.tsx b/src/pages/guard/mypage/index.tsx new file mode 100644 index 00000000..a02a9c87 --- /dev/null +++ b/src/pages/guard/mypage/index.tsx @@ -0,0 +1,22 @@ +import ProfileBox from './components/profile-box'; +import { PointBox, UseDetailBox } from '@/shared/components'; +import { Box } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +const GuardMyPage = () => { + return ( + + + + + + ); +}; + +export default GuardMyPage; + +const MyPageLayout = styled(Box)` + display: flex; + flex-direction: column; + align-items: center; +`; diff --git a/src/pages/guard/review/index.tsx b/src/pages/guard/review/index.tsx index 06a0b0b0..0a350ff8 100644 --- a/src/pages/guard/review/index.tsx +++ b/src/pages/guard/review/index.tsx @@ -1,8 +1,7 @@ import { useState } from 'react'; import starIcon from './asset/star-icon.svg'; -import { BasicButton } from '@/shared/components/common/button'; -import Notice from '@/shared/components/features/notice'; +import { BasicButton, Notice } from '@/shared/components'; import { Text, Flex, Box, Textarea, Image, Button } from '@chakra-ui/react'; import styled from '@emotion/styled'; @@ -22,18 +21,18 @@ const SinittoReviewPage = () => { return ( - + - + 시니또 정보 김춘식 / 22세 / 대학생 - + 평가하기 {[ @@ -75,11 +74,11 @@ const SinittoReviewPage = () => { - + 전체 평가 내용 (선택) - + 제출하기 @@ -98,7 +97,7 @@ const ReviewBox = styled(Box)` flex-direction: column; align-items: center; width: 100%; - max-width: 16rem; + max-width: 18rem; border: 1px solid var(--color-white-gray); border-radius: 10px; background-color: var(--color-white-gray); @@ -108,7 +107,7 @@ const ReviewBox = styled(Box)` const ReviewTextBox = styled(Textarea)` width: 100%; - max-width: 16rem; + max-width: 18rem; height: 110px; border: 1px solid var(--color-white-gray); border-radius: 10px; diff --git a/src/pages/sinitto/call-back/detail/components/menu/post-accept/index.tsx b/src/pages/sinitto/call-back/detail/components/menu/post-accept/index.tsx index 167734b3..5274e9aa 100644 --- a/src/pages/sinitto/call-back/detail/components/menu/post-accept/index.tsx +++ b/src/pages/sinitto/call-back/detail/components/menu/post-accept/index.tsx @@ -1,4 +1,4 @@ -import { BasicButton } from '@/shared/components/common/button'; +import { BasicButton } from '@/shared/components'; import styled from '@emotion/styled'; type Props = { diff --git a/src/pages/sinitto/call-back/detail/components/menu/pre-accept/index.tsx b/src/pages/sinitto/call-back/detail/components/menu/pre-accept/index.tsx index 7ed3412e..e761a13b 100644 --- a/src/pages/sinitto/call-back/detail/components/menu/pre-accept/index.tsx +++ b/src/pages/sinitto/call-back/detail/components/menu/pre-accept/index.tsx @@ -1,4 +1,4 @@ -import { BasicButton } from '@/shared/components/common/button'; +import { BasicButton } from '@/shared/components'; import styled from '@emotion/styled'; type Props = { diff --git a/src/pages/sinitto/call-back/detail/index.tsx b/src/pages/sinitto/call-back/detail/index.tsx index 43f7cd40..37e730ad 100644 --- a/src/pages/sinitto/call-back/detail/index.tsx +++ b/src/pages/sinitto/call-back/detail/index.tsx @@ -4,7 +4,7 @@ import { useParams, Outlet } from 'react-router-dom'; import { GuideLineList } from './components/guide-line-list'; import { PostAcceptMenu } from './components/menu/post-accept'; import { PreAcceptMenu } from './components/menu/pre-accept'; -import Notice from '@/shared/components/features/notice'; +import { Notice } from '@/shared/components'; import { Divider } from '@chakra-ui/react'; import styled from '@emotion/styled'; diff --git a/src/pages/sinitto/hello-call/hello-call-service/index.tsx b/src/pages/sinitto/hello-call/hello-call-service/index.tsx index 58f69c57..4b1cb526 100644 --- a/src/pages/sinitto/hello-call/hello-call-service/index.tsx +++ b/src/pages/sinitto/hello-call/hello-call-service/index.tsx @@ -5,7 +5,7 @@ import TitleImg from './asserts/title-icon.png'; import ServiceDetail from './components/service-detail'; import { SERVICE_NOTICE } from './data/notice'; import { RouterPath } from '@/app/routes/path'; -import Notice from '@/shared/components/features/notice'; +import { Notice } from '@/shared/components'; import { Box, Button, Divider, Image, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; diff --git a/src/pages/sinitto/mypage/components/account-info-box/index.tsx b/src/pages/sinitto/mypage/components/account-info-box/index.tsx new file mode 100644 index 00000000..2c10ea05 --- /dev/null +++ b/src/pages/sinitto/mypage/components/account-info-box/index.tsx @@ -0,0 +1,50 @@ +import { Box, Text } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +const AccountInfoBox = () => { + return ( + + + + 계좌번호 + + + 카카오뱅크 3333-3333-33333 + + + + + 계좌 인증 여부 + + + 인증 완료 + + + + ); +}; + +export default AccountInfoBox; + +const AccountBoxLayout = styled(Box)` + display: flex; + flex-direction: column; + justify-content: center; + background-color: #f2f2f2; + width: 100%; + max-width: 338px; + height: 100px; + border: 1px solid #909090; + border-radius: 5px; + margin-top: 0.5rem; +`; diff --git a/src/pages/sinitto/mypage/components/profile-box/index.tsx b/src/pages/sinitto/mypage/components/profile-box/index.tsx new file mode 100644 index 00000000..a07663bb --- /dev/null +++ b/src/pages/sinitto/mypage/components/profile-box/index.tsx @@ -0,0 +1,52 @@ +import { Box, Text } from '@chakra-ui/react'; +import styled from '@emotion/styled'; + +const ProfileBox = () => { + return ( + + + 시니또님 환영합니다. + + + + 이름 + + + 시니또 + + + + + 전화번호 + + + 010-1111-1111 + + + + ); +}; + +export default ProfileBox; + +const ProfileBoxLayout = styled(Box)` + display: flex; + flex-direction: column; + background-color: #f2f2f2; + width: 100%; + max-width: 338px; + height: 130px; + border: 1px solid #909090; + border-radius: 5px; + margin-top: 0.5rem; +`; diff --git a/src/pages/sinitto/mypage/index.tsx b/src/pages/sinitto/mypage/index.tsx index 6dcde85a..4604c7b2 100644 --- a/src/pages/sinitto/mypage/index.tsx +++ b/src/pages/sinitto/mypage/index.tsx @@ -1,20 +1,27 @@ -import PointBox from './components/PointBox'; -import ProfileBox from './components/ProfileBox'; -import UseDetailBox from './components/UseDetail'; +import AccountInfoBox from './components/account-info-box'; +import ProfileBox from './components/profile-box'; +import { BasicButton, PointBox, UseDetailBox } from '@/shared/components'; import { Box } from '@chakra-ui/react'; import styled from '@emotion/styled'; -const MyPage = () => { +const SinittoMypage = () => { return ( + + 내 정보 수정하기 + + + + 계좌번호 수정하기 + ); }; -export default MyPage; +export default SinittoMypage; const MyPageLayout = styled(Box)` display: flex; diff --git a/src/shared/api/auth/user-register.ts b/src/shared/api/auth/user-register.ts new file mode 100644 index 00000000..f628e925 --- /dev/null +++ b/src/shared/api/auth/user-register.ts @@ -0,0 +1,53 @@ +import { fetchInstance } from '../instance'; + +// request(요청) 타입 +export type SignupReguestParams = { + name: string; + phoneNumber: string; + email: string; + isSinitto: boolean; +}; + +// response(응답) 타입 - 성공 +export type SignupResponse = { + accessToken: string; + refreshToken: string; + isSinitto?: 'true' | 'false'; +}; + +// 에러(아직 정확히 에러코드 확인 x) or 예외 +export type SignupErrorResponse = { + status: number; + detail: string; +}; + +// 공통 타입 정의 (onSuccess 내부에서 분기) +export type SignupApiResponse = SignupResponse | SignupErrorResponse; + +export const registerUser = async ({ + name, + phoneNumber, + email, + isSinitto, +}: SignupReguestParams): Promise => { + try { + // 시니또 보호자에 따라 API 엔드포인트 구분 + const endpoint = isSinitto ? 'sinitto' : 'guard'; + + const response = await fetchInstance.post(`/api/members/${endpoint}`, { + name, + phoneNumber, + email, + isSinitto, + }); + if (response.status === 207) { + return { + status: response.status, + detail: response.data.detail, + } as SignupErrorResponse; // 207 (예외 - 중복 이메일) + } + return response.data; // 200 + } catch (err) { + throw new Error('회원가입 실패'); + } +}; diff --git a/src/shared/api/instance/index.ts b/src/shared/api/instance/index.ts index 439db8ec..6f4dcd9f 100644 --- a/src/shared/api/instance/index.ts +++ b/src/shared/api/instance/index.ts @@ -1,6 +1,11 @@ -import type { AxiosInstance, AxiosRequestConfig } from 'axios'; +import type { + AxiosInstance, + AxiosRequestConfig, + InternalAxiosRequestConfig, +} from 'axios'; import axios from 'axios'; +import { BASE_URI } from '@/shared/constants'; import { QueryClient } from '@tanstack/react-query'; const initInstance = (config: AxiosRequestConfig): AxiosInstance => { @@ -10,6 +15,8 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => { headers: { Accept: 'application/json', 'Content-Type': 'application/json', + 'Cross-Control-Allow-Origin': '*', + ...config.headers, }, }); @@ -18,7 +25,7 @@ const initInstance = (config: AxiosRequestConfig): AxiosInstance => { }; export const fetchInstance = initInstance({ - baseURL: 'https://api.example.com', + baseURL: BASE_URI, }); export const queryClient = new QueryClient({ @@ -31,3 +38,17 @@ export const queryClient = new QueryClient({ }, }, }); + +fetchInstance.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken !== undefined) { + config.headers['Content-Type'] = 'application/json'; + config.headers.Authorization = `Bearer ${accessToken}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); diff --git a/src/shared/components/common/button/index.tsx b/src/shared/components/common/button/BasicButton.tsx similarity index 100% rename from src/shared/components/common/button/index.tsx rename to src/shared/components/common/button/BasicButton.tsx diff --git a/src/shared/components/common/button/index.ts b/src/shared/components/common/button/index.ts new file mode 100644 index 00000000..c0cfceb8 --- /dev/null +++ b/src/shared/components/common/button/index.ts @@ -0,0 +1 @@ +export { BasicButton } from './BasicButton'; diff --git a/src/shared/components/common/index.ts b/src/shared/components/common/index.ts new file mode 100644 index 00000000..243b7344 --- /dev/null +++ b/src/shared/components/common/index.ts @@ -0,0 +1,2 @@ +export * from './button'; +export * from './layout'; diff --git a/src/shared/components/common/layout/Layout.tsx b/src/shared/components/common/layout/Layout.tsx new file mode 100644 index 00000000..3f1e1633 --- /dev/null +++ b/src/shared/components/common/layout/Layout.tsx @@ -0,0 +1,51 @@ +import { Outlet, useLocation } from 'react-router-dom'; + +import { RouterPath } from '@/app/routes/path'; +import { Header } from '@/shared/components'; + +const Layout = () => { + const location = useLocation(); + let title; + + // 현재 경로에 따라 제목을 설정 + switch (location.pathname) { + case RouterPath.REGISTER: + title = '회원가입'; + break; + case RouterPath.SINITTO_MYPAGE: + title = '마이페이지'; + break; + case RouterPath.GUARD_MYPAGE: + title = '마이페이지'; + break; + case RouterPath.GUARD_GUIDELINE: + title = '요청 가이드라인'; + break; + case RouterPath.SERVICE_HISTORY: + title = '서비스 이용 내역'; + break; + case RouterPath.HELLO_CALL: + title = '안부전화 서비스'; + break; + case RouterPath.SENIOR_REGISTER: + title = '시니어 등록'; + break; + case RouterPath.CALL_BACK_LIST: + title = '콜백 요청'; + break; + case RouterPath.SINITTO_REVIEW: + title = '시니또 평가'; + break; + default: + title = '나만의 작은 시니또'; + } + + return ( + <> +
+ + + ); +}; + +export default Layout; diff --git a/src/shared/components/common/layout/index.ts b/src/shared/components/common/layout/index.ts new file mode 100644 index 00000000..6c48faec --- /dev/null +++ b/src/shared/components/common/layout/index.ts @@ -0,0 +1 @@ +export { default as Layout } from './Layout'; diff --git a/src/shared/components/features/header/index.tsx b/src/shared/components/features/header/Header.tsx similarity index 100% rename from src/shared/components/features/header/index.tsx rename to src/shared/components/features/header/Header.tsx diff --git a/src/shared/components/features/header/index.ts b/src/shared/components/features/header/index.ts new file mode 100644 index 00000000..5653319d --- /dev/null +++ b/src/shared/components/features/header/index.ts @@ -0,0 +1 @@ +export { default as Header } from './Header'; diff --git a/src/shared/components/features/index.ts b/src/shared/components/features/index.ts new file mode 100644 index 00000000..2ffa5198 --- /dev/null +++ b/src/shared/components/features/index.ts @@ -0,0 +1,3 @@ +export * from './header'; +export * from './mypage'; +export * from './notice'; diff --git a/src/shared/components/features/mypage/hooks/index.ts b/src/shared/components/features/mypage/hooks/index.ts new file mode 100644 index 00000000..1689a4dc --- /dev/null +++ b/src/shared/components/features/mypage/hooks/index.ts @@ -0,0 +1 @@ +export { useToggleDetailBox } from './useToggleDetailBox'; diff --git a/src/shared/components/features/mypage/hooks/useToggleDetailBox.ts b/src/shared/components/features/mypage/hooks/useToggleDetailBox.ts new file mode 100644 index 00000000..f3fe55c1 --- /dev/null +++ b/src/shared/components/features/mypage/hooks/useToggleDetailBox.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; + +import { DUMMY_DATA } from '@/pages/sinitto/mypage/data/detail'; + +export const useToggleDetailBox = () => { + // 처음에 5개 보여주고, 더보기를 눌렀을 때 나머지 전부를 보여주기 + const [visibleCount, setVisibleCount] = useState(5); // 기본적으로 5개 보여줌 + const [enabled, setEnabled] = useState(false); // 더보기, 숨기기 상태 + + // 더보기, 숨기기 버튼을 눌렀을 때 동작 + const toggle = () => { + if (enabled) { + // 숨기기 동작 + setVisibleCount(5); + } else { + // 더보기 동작 + setVisibleCount(DUMMY_DATA.length); + } + setEnabled(!enabled); + }; + + const data = DUMMY_DATA.slice(0, visibleCount); + + return { data, enabled, toggle }; +}; diff --git a/src/shared/components/features/mypage/index.ts b/src/shared/components/features/mypage/index.ts new file mode 100644 index 00000000..93ba6edf --- /dev/null +++ b/src/shared/components/features/mypage/index.ts @@ -0,0 +1,3 @@ +export * from './hooks'; +export * from './point-box'; +export * from './use-detail'; diff --git a/src/pages/sinitto/mypage/components/PointBox/index.tsx b/src/shared/components/features/mypage/point-box/PointBox.tsx similarity index 100% rename from src/pages/sinitto/mypage/components/PointBox/index.tsx rename to src/shared/components/features/mypage/point-box/PointBox.tsx diff --git a/src/shared/components/features/mypage/point-box/index.ts b/src/shared/components/features/mypage/point-box/index.ts new file mode 100644 index 00000000..3381b17e --- /dev/null +++ b/src/shared/components/features/mypage/point-box/index.ts @@ -0,0 +1 @@ +export { default as PointBox } from './PointBox'; diff --git a/src/pages/sinitto/mypage/components/UseDetail/index.tsx b/src/shared/components/features/mypage/use-detail/UseDetailBox.tsx similarity index 64% rename from src/pages/sinitto/mypage/components/UseDetail/index.tsx rename to src/shared/components/features/mypage/use-detail/UseDetailBox.tsx index e9a4c1e3..cbd5e610 100644 --- a/src/pages/sinitto/mypage/components/UseDetail/index.tsx +++ b/src/shared/components/features/mypage/use-detail/UseDetailBox.tsx @@ -1,39 +1,7 @@ -import { useState } from 'react'; - -import { DUMMY_DATA } from '../../data/detail'; +import { useToggleDetailBox } from '../hooks/useToggleDetailBox'; import { Box, Text } from '@chakra-ui/react'; import styled from '@emotion/styled'; -// 컴포넌트는 data를 어떻게 처리하는지에 대한 관심이 없다. -// output만 있으면 되는.. -// output을 만들어내는 과정을 분리하면 어떨까? - -// 관심사를 분리하기 -const useToggleDetailBox = () => { - // 처음에 5개 보여주고, 더보기를 눌렀을 때 나머지 전부를 보여주기 - const [visibleCount, setVisibleCount] = useState(5); // 기본적으로 5개 보여줌 - const [enabled, setEnabled] = useState(false); // 더보기, 숨기기 상태 - - // 더보기, 숨기기 버튼을 눌렀을 때 동작 - // toggleView 라는 이벤트가 발생했을 때 처리하는 이벤트 핸들러가 handleToggleView 인데 - // 이건 toggleView라는 이벤트가 발생했을 때 처리하는게 아니라, toggleView라는 행위를 하는 함수임. - const toggle = () => { - if (enabled) { - // 숨기기 동작 - setVisibleCount(5); - } else { - // 더보기 동작 - setVisibleCount(DUMMY_DATA.length); - } - setEnabled(!enabled); - }; - - const data = DUMMY_DATA.slice(0, visibleCount); - - return { data, enabled, toggle }; -}; - -// 컴포넌트는 데이터 처리에 대해 관심있는게 아니라, "어떤 데이터"를 가지고 그리는지에 관심 있음 const UseDetailBox = () => { const { data, enabled, toggle } = useToggleDetailBox(); @@ -95,6 +63,8 @@ const DetailBox = styled(Box)` flex-direction: column; align-items: center; width: 100%; + // TODO: 높이 지정해주세요. + /* height : */ max-width: 338px; `; @@ -103,7 +73,7 @@ const DetailFactor = styled(Box)` flex-direction: row; width: 100%; max-width: 310px; - height: 2.5rem; + min-height: 55px; background-color: #fff; margin: 0.3rem 0; border: 1px solid #fff; diff --git a/src/shared/components/features/mypage/use-detail/index.ts b/src/shared/components/features/mypage/use-detail/index.ts new file mode 100644 index 00000000..6961f8d0 --- /dev/null +++ b/src/shared/components/features/mypage/use-detail/index.ts @@ -0,0 +1 @@ +export { default as UseDetailBox } from './UseDetailBox'; diff --git a/src/shared/components/features/notice/index.tsx b/src/shared/components/features/notice/Notice.tsx similarity index 100% rename from src/shared/components/features/notice/index.tsx rename to src/shared/components/features/notice/Notice.tsx diff --git a/src/shared/components/features/notice/index.ts b/src/shared/components/features/notice/index.ts new file mode 100644 index 00000000..85474929 --- /dev/null +++ b/src/shared/components/features/notice/index.ts @@ -0,0 +1 @@ +export { default as Notice } from './Notice'; diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts new file mode 100644 index 00000000..1aacf692 --- /dev/null +++ b/src/shared/components/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './features'; diff --git a/src/shared/constants/.gitkeep b/src/shared/constants/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/shared/constants/URI.ts b/src/shared/constants/URI.ts new file mode 100644 index 00000000..1512962c --- /dev/null +++ b/src/shared/constants/URI.ts @@ -0,0 +1,3 @@ +export const BASE_URI = import.meta.env.VITE_APP_BASE_URL; + +export const KAKAO_AUTH_URL: string = `${BASE_URI}/api/auth/oauth/kakao`; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts new file mode 100644 index 00000000..28073524 --- /dev/null +++ b/src/shared/constants/index.ts @@ -0,0 +1 @@ +export * from './URI'; diff --git a/src/shared/utils/storage/index.ts b/src/shared/utils/storage/index.ts new file mode 100644 index 00000000..c562a0b8 --- /dev/null +++ b/src/shared/utils/storage/index.ts @@ -0,0 +1,24 @@ +type StorageKey = { + accessToken?: string; +}; + +const initStorage = (key: T, storage: Storage) => { + const storageKey = `${key}`; + + const get = (): StorageKey[T] => { + const value = storage.getItem(storageKey); + return value as StorageKey[T]; + }; + + const set = (value: StorageKey[T]) => { + if (value == undefined || value == null) { + return storage.removeItem(storageKey); + } + const stringifiedValue = JSON.stringify(value); + storage.setItem(storageKey, stringifiedValue); + }; + + return { get, set }; +}; + +export const authLocalStorage = initStorage('accessToken', localStorage);