Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
"preview": "vite preview"
},
"dependencies": {
"amazon-cognito-identity-js": "^6.3.12",
"axios": "^1.8.4",
"msw": "^2.7.3",
"path": "^0.12.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.3.0",
"styled-components": "^6.1.16"
"styled-components": "^6.1.16",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/node": "^22.13.10",
"@types/node": "^22.13.11",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-router-dom": "^5.3.3",
Expand Down
19 changes: 19 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from "axios";

const API_BASE_URL =
"https://5erhg0u08g.execute-api.ap-northeast-2.amazonaws.com";

const API = axios.create({
baseURL: API_BASE_URL,
});

// 요청 시 Authorization 헤더 추가
API.interceptors.request.use((config) => {
const token = localStorage.getItem("access_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

export default API;
67 changes: 67 additions & 0 deletions src/hooks/uaeSignup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as AWSCognitoIdentity from "amazon-cognito-identity-js";
import signIn from "@/hooks/usrLogin";

const userPoolData: AWSCognitoIdentity.ICognitoUserPoolData = {
UserPoolId: "ap-northeast-2_36GFLZBtI",
ClientId: "7j09fmn00udb6fpt8hhij1jlqk",
};

export async function signUp({
Username,
Password,
Email,
}: {
Username: string;
Password: string;
Email: string;
}): Promise<{ message: string }> {
/*
* Required attributes를 추가
* */
const attributeData: AWSCognitoIdentity.ICognitoUserAttributeData = {
Name: "nickname",
Value: Username,
};

let attributeList: AWSCognitoIdentity.CognitoUserAttribute[] = [
new AWSCognitoIdentity.CognitoUserAttribute(attributeData),
];
/*
* CognitoUserPool.signUp() 함수에 다음과 같이 Username, Password, Required attributes를 전달
* 콜백함수를 통해 결과를 반환
* */
return await new Promise((resolve, reject) => {
const userPool = new AWSCognitoIdentity.CognitoUserPool(userPoolData);

userPool.signUp(
Email,
Password,
attributeList,
attributeList,
(
err: Error | undefined,
result: AWSCognitoIdentity.ISignUpResult | undefined
): void => {
if (err) reject({ message: err.message || JSON.stringify(err) });
else {
resolve({
message:
result?.user.getUsername() +
"님, 회원 가입이 성공적으로 완료되었습니다.",
});

signIn(Email, Password);

while (!localStorage.getItem("access_token")) {
continue;
}

console.log(
"AccessToken in signup:",
localStorage.getItem("access_token")
);
}
}
);
});
}
42 changes: 42 additions & 0 deletions src/hooks/usrLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
CognitoUserPool,
CognitoUser,
AuthenticationDetails,
} from "amazon-cognito-identity-js";

const poolData = {
UserPoolId: "ap-northeast-2_36GFLZBtI",
ClientId: "7j09fmn00udb6fpt8hhij1jlqk",
};
const userPool = new CognitoUserPool(poolData);

const signIn = (username: string, password: string) => {
return new Promise((resolve, reject) => {
const user = new CognitoUser({
Username: username,
Pool: userPool,
});

const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
});

user.authenticateUser(authDetails, {
onSuccess: (session) => {
console.log("로그인 성공", session);
const accessToken = session.getAccessToken().getJwtToken();
localStorage.setItem("access_token", accessToken);
console.log("AccessToken in login:", accessToken);

resolve(accessToken);
},
onFailure: (err) => {
console.error("로그인 실패:", err.message);
reject(err);
},
});
});
};

export default signIn;
13 changes: 7 additions & 6 deletions src/pages/LoginPage/LoginPage.style.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import styled from 'styled-components';
import styled from "styled-components";

// 전체 컨테이너
export const AppContainer = styled.div`
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
width: 100vw;
min-height: 100vh;
margin: 0;
Expand Down Expand Up @@ -38,7 +39,7 @@ export const Main = styled.main`
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 60px);
min-height: calc(100vh - 200px);
padding: 20px;
width: 100%;
box-sizing: border-box;
Expand Down Expand Up @@ -144,7 +145,7 @@ export const ForgotPassword = styled.a`
font-size: 13px;
color: #555;
text-decoration: none;

&:hover {
text-decoration: underline;
}
Expand Down Expand Up @@ -178,8 +179,8 @@ export const RegisterLink = styled.a`
color: #4a7bff;
text-decoration: none;
font-weight: 500;

&:hover {
text-decoration: underline;
}
`;
`;
140 changes: 105 additions & 35 deletions src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,145 @@
// App.tsx
import React from 'react';
import LoginIllustration from '../../assets/login-illustration.svg';
import BookVector from '../../assets/book_vector.svg'
import { useNavigate } from 'react-router-dom';
import { useState } from "react";
import LoginIllustration from "../../assets/login-illustration.svg";
import logo from "../../assets/image/logo.svg";
import { useNavigate } from "react-router-dom";
import signIn from "@/hooks/usrLogin";

// Styled Components
import {
AppContainer, Header, HeaderContent, BookLogo, HeaderLogo,
Main, ContentContainer, ImageSection, FormSection,
WelcomeText, Form, InputGroup, Input, PasswordInput,
EyeIcon, RememberSection, CheckboxLabel, Checkbox,
ForgotPassword, LoginButton, RegisterSection, RegisterLink
} from './LoginPage.style';
AppContainer,
Header,
Main,
ContentContainer,
ImageSection,
FormSection,
WelcomeText,
Form,
InputGroup,
Input,
PasswordInput,
EyeIcon,
RememberSection,
CheckboxLabel,
Checkbox,
ForgotPassword,
LoginButton,
RegisterSection,
RegisterLink,
} from "./LoginPage.style";

const LoginPage: React.FC = () => {
let navigate = useNavigate();

const [formData, setFormData] = useState({
email: "",
password: "",
});
const [error, setError] = useState<string>("");

// 이메일, 비밀번호 입력값 상태 업데이트 함수
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};

const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); // 폼 제출 기본 동작 방지

if (!formData.email || !formData.password) {
setError("이메일과 비밀번호를 모두 입력해주세요.");
return;
}

try {
// 로그인 API 호출
await signIn(formData.email, formData.password);

if (localStorage.getItem("access_token")) {
// 로그인 성공 시, 대시보드로 이동
navigate("/main");
} else {
setError("로그인 실패. 이메일 또는 비밀번호를 확인해주세요.");
}
} catch (err: any) {
console.error("로그인 중 오류 발생:", err);
setError("로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};

return (
<AppContainer>
<Header>
<HeaderContent>
<BookLogo src={BookVector} alt="Book Logo" />
<HeaderLogo>XRPedia</HeaderLogo>
</HeaderContent>
<img src={logo} alt="Book Logo" />
</Header>

<Main>
<ContentContainer>
<ImageSection>
<img
src={LoginIllustration}
alt="Woman interacting with documents"
style={{ maxWidth: '100%', height: 'auto' }}
<img
src={LoginIllustration}
alt="Woman interacting with documents"
style={{ maxWidth: "100%", height: "auto" }}
/>
</ImageSection>

<FormSection>
<WelcomeText>
XRPedia에 온 것을 환영해요! 👋
</WelcomeText>

<Form>
<WelcomeText>XRPedia에 온 것을 환영해요! 👋</WelcomeText>

<Form onSubmit={handleLogin}>
{error && <p style={{ color: "red" }}>{error}</p>}
<InputGroup>
<Input type="email" placeholder="e-mail" />
<Input
type="email"
name="email"
placeholder="e-mail"
value={formData.email}
onChange={handleInputChange}
/>
</InputGroup>

<PasswordInput>
<Input type="password" placeholder="password" />
<Input
type="password"
name="password"
placeholder="password"
value={formData.password}
onChange={handleInputChange}
/>
<EyeIcon>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</EyeIcon>
</PasswordInput>

<RememberSection>
<CheckboxLabel>
<Checkbox type="checkbox" />
로그인 상태 유지
</CheckboxLabel>

<ForgotPassword href="#">비밀번호 찾기</ForgotPassword>
</RememberSection>

<LoginButton type="submit">로그인</LoginButton>
</Form>

<RegisterSection>
<span>계정이 없으신가요?</span>
<RegisterLink onClick={() => navigate('/signup')}>회원가입</RegisterLink>
<RegisterLink onClick={() => navigate("/signup")}>
회원가입
</RegisterLink>
</RegisterSection>
</FormSection>
</ContentContainer>
Expand Down
Loading