Skip to content
Merged
32 changes: 31 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"react-naver-maps": "^0.1.3",
"react-router-dom": "^7.1.1",
"react-table": "^7.8.0",
"styled-components": "^6.1.14"
"styled-components": "^6.1.14",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Router from './Router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import GlobalStyle from './styles/globalStyle';
import './styles/font.css';
import { NotificationProvider } from './components/NotificationProvider';
function App() {
const queryClient = new QueryClient();
return (
Expand All @@ -14,6 +15,7 @@ function App() {
<Router />
</ThemeProvider>
</QueryClientProvider>
<NotificationProvider />
</>
);
}
Expand Down
1 change: 0 additions & 1 deletion src/apis/slopeManage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const slopeManageAPI = {
},
});
console.log('엑셀 업로드', response);
alert(`${response.data.message}\n${response.data.count}건 추가되었습니다.`);
return response.data;
},
createSlope: async (newSlope: Slope) => {
Expand Down
41 changes: 41 additions & 0 deletions src/components/NotificationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import { useNotificationStore } from '../hooks/notificationStore';

export const NotificationProvider = () => {
const { isOpen, message, severity, autoHideDuration, hideNotification } =
useNotificationStore();

// 컴포넌트 언마운트 시 알림 초기화 (선택 사항)
useEffect(() => {
return () => {
if (isOpen) {
hideNotification();
}
};
}, []);

const handleClose = (
_event?: React.SyntheticEvent | Event,
reason?: string
) => {
if (reason === 'clickaway') {
return;
}
hideNotification();
};

return (
<Snackbar
open={isOpen}
autoHideDuration={autoHideDuration}
onClose={handleClose}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Alert onClose={handleClose} severity={severity} variant="filled">
{message}
</Alert>
</Snackbar>
);
};
37 changes: 37 additions & 0 deletions src/hooks/notificationStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { create } from 'zustand';
import { AlertColor } from '@mui/material/Alert';

interface NotificationState {
isOpen: boolean;
message: string;
severity: AlertColor;
autoHideDuration: number | null;
showNotification: (
message: string,
options?: {
severity?: AlertColor;
autoHideDuration?: number | null;
}
) => void;
hideNotification: () => void;
}

export const useNotificationStore = create<NotificationState>((set) => ({
isOpen: false,
message: '',
severity: 'info',
autoHideDuration: 4000,

showNotification: (message, options = {}) =>
set({
isOpen: true,
message,
severity: options.severity || 'info',
autoHideDuration:
options.autoHideDuration !== undefined
? options.autoHideDuration
: 4000,
}),

hideNotification: () => set({ isOpen: false }),
}));
13 changes: 12 additions & 1 deletion src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Join from './components/Join';

const LoginPage = () => {
const [isLogin, setIsLogin] = useState<boolean>(true);

return (
<Background>
<LoginContainer>
Expand All @@ -30,7 +31,15 @@ const LoginPage = () => {
</TabButton>
</TabContainer>

{isLogin ? <Login /> : <Join />}
{isLogin ? (
<Login />
) : (
<Join
completeJoin={() => {
setIsLogin(true);
}}
/>
)}
</LoginContainer>
</Background>
);
Expand All @@ -44,6 +53,8 @@ const Background = styled.div`
display: flex;
justify-content: center;
align-items: center;
overflow-x: hidden;
padding: 10px 0px;
`;
const LogoContainer = styled.img`
width: 100%;
Expand Down
26 changes: 20 additions & 6 deletions src/pages/LoginPage/components/Join.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { authAPI, JoinFormType } from '../../../apis/Auth';
import styled from 'styled-components';
import PrivacyPolicyModal from '../../MapPage/components/map/PrivacyPolicyModal';
import TermsofUseModal from '../../MapPage/components/map/TermsofUseModal';
import { useNotificationStore } from '../../../hooks/notificationStore';

const Join = () => {
interface joinPropsType {
completeJoin: () => void;
}
const Join = ({ completeJoin }: joinPropsType) => {
const [joinForm, setJoinForm] = useState<JoinFormType>({
name: '',
phone: '',
Expand All @@ -25,6 +29,10 @@ const Join = () => {
const [agreementError, setAgreementError] = useState<boolean>(false);
const [isPrivacyPolicyOpen, setIsPrivacyPolicyOpen] = useState(false);
const [isTermofUseOpen, setIsTermofUseOpen] = useState(false);
const showNotification = useNotificationStore(
(state) => state.showNotification
);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;

Expand All @@ -39,7 +47,7 @@ const Join = () => {
[name]: value,
}));
}
console.log(joinForm);
// console.log(joinForm);
};

const handlePwCheckChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -84,7 +92,7 @@ const Join = () => {
const joinMutation = useMutation({
mutationFn: (data: JoinFormType) => authAPI.join(data),
onSuccess: () => {
alert('회원가입이 완료되었습니다.');
showNotification('회원가입 성공!', { severity: 'success' });
setJoinForm({
name: '',
phone: '',
Expand All @@ -98,9 +106,13 @@ const Join = () => {
terms: false,
privacy: false,
});
completeJoin();
},
onError: (error) => {
alert('회원가입에 실패했습니다. 다시 시도해주세요.');
onError: (error: any) => {
const errorMes =
error.response?.data?.message ||
'회원가입에 실패했습니다. 다시 시도해주세요.';
showNotification(errorMes, { severity: 'error', autoHideDuration: 6000 });
console.error('join Error:', error);
},
});
Expand All @@ -116,7 +128,9 @@ const Join = () => {
if (pwVerfiy && isFormFilled && isAgreementsChecked) {
joinMutation.mutate(joinForm);
} else {
alert('정보를 정확히 입력하고 필수 약관에 동의해주세요.');
showNotification('정보를 정확히 입력하고 필수 약관에 동의해주세요.', {
severity: 'error',
});
}
};

Expand Down
80 changes: 46 additions & 34 deletions src/pages/LoginPage/components/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { ErrorText, Input, InputWrapper, LoginButton } from './Style';
import { useMutation } from '@tanstack/react-query';
import { authAPI, LoginFormType } from '../../../apis/Auth';
import { useNotificationStore } from '../../../hooks/notificationStore';

const Login = () => {
const nav = useNavigate();
Expand All @@ -11,20 +12,29 @@ const Login = () => {
password: '',
});
const [isPhoneValid, setIsPhoneValid] = useState<boolean>(true);

const showNotification = useNotificationStore(
(state) => state.showNotification
);
const joinMutation = useMutation({
mutationFn: (data: LoginFormType) => authAPI.login(data),
onSuccess: () => {
alert('로그인 성공');
showNotification('로그인 성공!', { severity: 'success' });
setLoginForm({
phone: '',
password: '',
});

nav('/manage/map');
},
onError: (error) => {
alert('로그인에 실패했습니다. 다시 시도해주세요.');
console.error('join Error:', error);
onError: (error: any) => {
const errorMes =
error.response?.data?.message ||
'로그인에 실패했습니다. 다시 시도해주세요.';
showNotification(errorMes, {
severity: 'error',
autoHideDuration: 6000,
});
console.error('login Error:', error);
},
});

Expand All @@ -35,7 +45,7 @@ const Login = () => {
if (isFormFilled) {
joinMutation.mutate(loginForm);
} else {
alert('빈칸 없이 입력해주세요.');
showNotification('빈칸 없이 입력해주세요', { severity: 'warning' });
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -58,34 +68,36 @@ const Login = () => {
}
};
return (
<InputWrapper>
<Input
name="phone"
value={loginForm.phone}
placeholder="전화번호"
type="number"
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === '-' || e.key === '+' || e.key === 'e') {
e.preventDefault();
setIsPhoneValid(false);
setTimeout(() => setIsPhoneValid(true), 2000);
}
}}
/>
{!isPhoneValid && (
<ErrorText>"-"를 제외한 숫자만 입력해 주세요</ErrorText>
)}
<Input
name="password"
value={loginForm.password}
placeholder="비밀번호"
type="password"
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<LoginButton onClick={onSubmit}>로그인</LoginButton>
</InputWrapper>
<>
<InputWrapper>
<Input
name="phone"
value={loginForm.phone}
placeholder="전화번호"
type="number"
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === '-' || e.key === '+' || e.key === 'e') {
e.preventDefault();
setIsPhoneValid(false);
setTimeout(() => setIsPhoneValid(true), 2000);
}
}}
/>
{!isPhoneValid && (
<ErrorText>"-"를 제외한 숫자만 입력해 주세요</ErrorText>
)}
<Input
name="password"
value={loginForm.password}
placeholder="비밀번호"
type="password"
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<LoginButton onClick={onSubmit}>로그인</LoginButton>
</InputWrapper>
</>
);
};

Expand Down
Loading
Loading