Skip to content

Commit

Permalink
Merge pull request #99 from boostcampwm-2024/fe/feature/Auth_Lotto_fu…
Browse files Browse the repository at this point in the history
…nctions

[FE/feature] auth + lotto 기능 완성
  • Loading branch information
edder773 authored Nov 25, 2024
2 parents 41899c5 + 6eb23d5 commit c3e87fd
Show file tree
Hide file tree
Showing 21 changed files with 422 additions and 132 deletions.
6 changes: 3 additions & 3 deletions apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ErrorExceptionFilter } from './global/filter/errorExceptionFilter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
// import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -16,9 +16,9 @@ async function bootstrap() {
.build();

const document = SwaggerModule.createDocument(app, config);
const configService = app.get(ConfigService);
// const configService = app.get(ConfigService);
app.enableCors({
origin: configService.get('CORS_ORIGIN'),
origin: true,
credentials: true
});

Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CropMarket from '@/pages/CropMarket';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import PrivateRoute from '@/components/ProtectRoute';
import OauthLogin from '@/components/OauthLogin';
import { UserProvider } from '@/components/UserContext';

interface LayoutProps {
Expand Down Expand Up @@ -36,6 +37,7 @@ const Layout: React.FC<LayoutProps> = ({ path, children }) => {

const routes = [
{ path: '/', element: <Intro /> },
{ path: 'oauth/redirect', element: <OauthLogin /> },
...[
{ path: '/main', element: <PrivateRoute element={<Main />} /> },
{ path: '/lottery', element: <PrivateRoute element={<Lottery />} /> },
Expand Down
38 changes: 25 additions & 13 deletions apps/frontend/src/components/EditNicknameModal.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
import { useState } from 'react';
import CloseIcon from './CloseIcon';
import { updateNickname } from '@/services/AuthApi';
import { useUser } from './UserContext';

interface EditNicknameModalProps {
isOpen: boolean;
id: string;
setId: React.Dispatch<React.SetStateAction<string>>;
modalOpen: () => void;
}

const EditNicknameModal: React.FC<EditNicknameModalProps> = ({ isOpen, id, setId, modalOpen }) => {
const [tempId, setTempId] = useState<string>('농부왕');
const EditNicknameModal: React.FC<EditNicknameModalProps> = ({ isOpen, modalOpen }) => {
const { nickname, setNickname } = useUser();
const [tmpNickname, setTmpNickname] = useState<string>(nickname);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const handleNicknameChange = () => {
if (tempId.length < 2) {
setErrorMessage('닉네임은 최소 2자 이상이어야 합니다.');
const handleNicknameChange = async () => {
if (tmpNickname === nickname) {
modalOpen();
return;
}

setErrorMessage(null);
setId(tempId);
try {
const response = await updateNickname({ nickname: tmpNickname });
if (response.success) {
setErrorMessage(null);
setNickname(tmpNickname);

modalOpen();
modalOpen();
}
} catch (error) {
if (error instanceof Error) {
setErrorMessage(error.message || '서버와의 연결에 실패했습니다.');
} else {
setErrorMessage('서버와의 연결에 실패했습니다.');
}
}
};

const handleCancelEdit = () => {
setTempId(id);
setTmpNickname(nickname);
setErrorMessage(null);
modalOpen();
};
Expand All @@ -42,9 +54,9 @@ const EditNicknameModal: React.FC<EditNicknameModalProps> = ({ isOpen, id, setId
<div className="flex flex-col px-4 gap-2">
<p className="font-bold text-lg">변경할 닉네임을 작성해주세요!</p>
<input
onChange={e => setTempId(e.target.value)}
onChange={e => setTmpNickname(e.target.value)}
className="rounded px-4 py-1 text-base text-center min-w-[200px] "
value={tempId}
value={tmpNickname}
/>
{errorMessage && <div className="text-sm text-red-600">{errorMessage}</div>}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const Header: React.FC = () => {
</section>

<section className="flex items-center justify-center bg-light-beige text-lg font-semibold border-4 border-light-pink rounded-2xl p-3 mx-4 min-w-[200px] max-w-[400px]">
<p>{totalAssets}</p>
<p>{totalAssets.toLocaleString()}</p>
</section>
</div>

Expand Down
35 changes: 26 additions & 9 deletions apps/frontend/src/components/LoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ const LoginModal: React.FC<LoginProps> = ({ setModalStep }) => {
}
};

const handleGoogle = async () => {
window.location.href = 'http://localhost:8080/api/auth/google';
};

const handleKakao = async () => {
window.location.href = 'http://localhost:8080/api/auth/kakao';
};

return (
<>
<img src="/icon.png" className="max-w-[200px] max-h-[200px] mb-4"></img>
Expand Down Expand Up @@ -88,20 +96,26 @@ const LoginModal: React.FC<LoginProps> = ({ setModalStep }) => {

<div className="flex items-center w-full text-black text-xs font-semibold my-4 select-none">
<span className="flex-grow h-px bg-black mx-4"></span>
social login
소셜 로그인
<span className="flex-grow h-px bg-black mx-4"></span>
</div>

<div className="flex flex-row my-2 gap-16">
<button className="rounded-full border-none w-[40px] h-[40px] overflow-hidden bg-gray-200 flex items-center justify-center">
<button
className="rounded-full border-none w-[40px] h-[40px] overflow-hidden bg-gray-200 flex items-center justify-center"
onClick={handleGoogle}
>
<img
src="https://d1nuzc1w51n1es.cloudfront.net/d99d8628713bb69bd142.png"
alt="google login"
className="w-full h-full object-cover"
/>
</button>

<button className="rounded-full border-none w-[40px] h-[40px] overflow-hidden bg-gray-200 flex items-center justify-center">
<button
className="rounded-full border-none w-[40px] h-[40px] overflow-hidden bg-gray-200 flex items-center justify-center"
onClick={handleKakao}
>
<img
src="https://d1nuzc1w51n1es.cloudfront.net/c9b51919f15c93b05ae8.png"
alt="kakao login"
Expand All @@ -110,12 +124,15 @@ const LoginModal: React.FC<LoginProps> = ({ setModalStep }) => {
</button>
</div>

<p
className="my-2 cursor-pointer hover:underline text-xs text-black font-semibold"
onClick={() => setModalStep(ModalStep.SignUpStep1)}
>
Don't have an account?
</p>
<div className="flex flex-row my-2 gap-2">
<p className="my-2 text-xs text-black font-semibold">역전농부는 처음이신가요?</p>
<p
className="my-2 cursor-pointer hover:underline text-xs text-blue-600 font-semibold"
onClick={() => setModalStep(ModalStep.SignUpStep1)}
>
회원가입
</p>
</div>
</>
);
};
Expand Down
9 changes: 5 additions & 4 deletions apps/frontend/src/components/LotteryTicket.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import LotteryIcon from './LotteryIcon';
import { WIDTH, HEIGHT } from '@/constants/LotteryConstants';
import { WIDTH, HEIGHT, WINNINGS } from '@/constants/LotteryConstants';

interface LotteryTicketProps {
isCanvasVisible: boolean;
canvasRef: React.RefObject<HTMLCanvasElement>;
rank: number;
}

const LotteryTicket: React.FC<LotteryTicketProps> = ({ isCanvasVisible, canvasRef }) => {
const LotteryTicket: React.FC<LotteryTicketProps> = ({ isCanvasVisible, canvasRef, rank }) => {
return (
<div className="relative flex flex-col items-center justify-center">
<LotteryIcon>
Expand All @@ -24,8 +25,8 @@ const LotteryTicket: React.FC<LotteryTicketProps> = ({ isCanvasVisible, canvasRe
{isCanvasVisible && (
<div className="absolute top-[20px] left-[280px] w-[500px] h-[270px]">
<div className="absolute flex flex-col top-0 left-0 w-full h-full flex items-center justify-center text-2xl font-bold bg-white text-black rounded-lg">
<p>성공</p>
<p>+ 100,000,000원!</p>
<p>{rank === 5 ? '실패!' : '성공!'}</p>
{rank !== 5 && <p>+ {WINNINGS[rank][0]}!</p>}
</div>
<canvas
ref={canvasRef}
Expand Down
33 changes: 33 additions & 0 deletions apps/frontend/src/components/OauthLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from 'react';
import { useUser } from './UserContext';

const OauthLogin: React.FC = () => {
const { setNickname } = useUser();

useEffect(() => {
const handleCallback = () => {
const urlParams = new URLSearchParams(window.location.search);
const nickname = urlParams.get('nickname');
const accessToken = urlParams.get('accessToken');
const refreshToken = urlParams.get('refreshToken');

if (accessToken && refreshToken && nickname) {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setNickname(nickname);

setTimeout(() => {
window.location.href = '/main';
}, 100);
}
};

if (window.location.search.includes('accessToken=')) {
handleCallback();
}
}, []);

return <></>;
};

export default OauthLogin;
61 changes: 55 additions & 6 deletions apps/frontend/src/components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import EditIcon from '@/components/EditIcon';
import SaveIcon from './SaveIcon';
import { updateIntroduce } from '@/services/AuthApi';
import { getMyRank } from '@/services/RankApi';

interface ProfileProps {
id: string;
modalOpen: () => void;
}

const Profile: React.FC<ProfileProps> = ({ id, modalOpen }) => {
const [tmpIntro, setTmpIntro] = useState<string>('안녕하세요! 농부왕의 농장입니다!');
const [introduce, setIntroduce] = useState<string>('안녕하세요! 농부왕의 농장입니다!');
const [isEditable, setIsEditable] = useState<boolean>(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [myRank, setMyRank] = useState<number>(0);
const [error, setError] = useState<string | null>('데이터 로딩 중 오류가 발생했습니다.');

useEffect(() => {
const fetchMyRank = async () => {
try {
const response = await getMyRank();
if (response.success) {
setMyRank(response.rank || 0);
setError(null);
} else {
setError(response.message || '데이터 로딩 중 오류가 발생했습니다.');
}
} catch (error) {
if (error instanceof Error) {
setError(error.message || '서버와의 연결에 실패했습니다.');
} else {
setError('서버와의 연결에 실패했습니다.');
}
}
};

fetchMyRank();
}, []);

const editIntroduce = () => {
if (!isEditable) {
Expand All @@ -19,15 +46,37 @@ const Profile: React.FC<ProfileProps> = ({ id, modalOpen }) => {
textareaRef.current.focus();
}
} else {
if (tmpIntro != introduce) {
changeIntroduce(tmpIntro);
}
setIsEditable(false);
}
};

const changeIntroduce = async (newIntroduce: string) => {
try {
const response = await updateIntroduce({ introduce: newIntroduce });
if (response.success) {
setIntroduce(newIntroduce);
}
} catch {
alert('소개글 변경에 실패했습니다. 다시 시도해주세요.');
}
};

return (
<div className="flex flex-col items-center bg-light-beige border-4 border-light-pink rounded-2xl p-6 w-[300px]">
<div className="relative w-16 h-16 flex items-center justify-center mb-2">
<img src="/trophy.png" className="absolute w-full h-full object-contain" alt="Trophy" />
<p className="absolute text-red-soft text-2xl font-bold">10000</p>
<div className="relative w-full h-16 flex items-center justify-center mb-2">
{error ? (
<p className="absolute text-red-soft font-bold">{error}</p>
) : (
<>
<img src="/trophy.png" className="absolute w-full h-full object-contain" alt="Trophy" />
<p className="absolute top-2 text-red-soft text-2xl font-bold">
{myRank === -1 ? 'UnRank' : myRank}
</p>
</>
)}
</div>
<div className="flex flex-row items-center justify-center gap-2">
<p className="text-lg font-bold">{id}</p>
Expand All @@ -37,8 +86,8 @@ const Profile: React.FC<ProfileProps> = ({ id, modalOpen }) => {
<textarea
ref={textareaRef}
className={`bg-light-red text-red-soft font-bold p-1 mt-2 mx-2 w-full resize-none rounded ${isEditable ? 'border-2 border-blue-500' : 'border-none'}`}
value={introduce}
onChange={e => setIntroduce(e.target.value)}
value={tmpIntro}
onChange={e => setTmpIntro(e.target.value)}
readOnly={!isEditable}
/>
{isEditable ? (
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/components/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ interface UserProviderProps {
children: ReactNode;
}

// 수정 필요
// localstorage 보다 서버에 요청해서 받아오는 코드 + 페이지 넘어갈 때마다 reload 시키는 코드로 변경
// 지금은 user1으로 로그인 및 로그아웃 -> user2로 로그인시 이상하게 동작
export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
const [nickname, setNickname] = useState<string>(() => {
const savedNickname = localStorage.getItem('nickname');
Expand Down
8 changes: 8 additions & 0 deletions apps/frontend/src/constants/LotteryConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ export const ERASE_RADIUS = 40;
export const ERASE_DISTANCE = 5;
export const WIDTH = 500;
export const HEIGHT = 270;
export const PRICE: number = 1000;
export const WINNINGS: Record<number, [number, string]> = {
1: [500000, 'first_count'],
2: [200000, 'second_count'],
3: [140000, 'third_count'],
4: [100000, 'fourth_count'],
5: [0, 'fifth_count']
};
8 changes: 6 additions & 2 deletions apps/frontend/src/hooks/UseLotteryCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useRef, useState, useEffect } from 'react';
import { ERASE_RADIUS, ERASE_DISTANCE, WIDTH, HEIGHT } from '@/constants/LotteryConstants';

const UseLotteryCanvas = () => {
const [isCanvasVisible, setIsCanvasVisible] = useState(false);
const [isScratching, setIsScratching] = useState(false);
const [isCanvasVisible, setIsCanvasVisible] = useState<boolean>(false);
const [isScratching, setIsScratching] = useState<boolean>(false);
const [isClear, setIsClear] = useState<boolean>(false);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const isDrawing = useRef(false);
const erasedCount = useRef(0);
Expand All @@ -12,6 +13,7 @@ const UseLotteryCanvas = () => {
const resetLottery = () => {
setIsCanvasVisible(false);
setIsScratching(false);
setIsClear(false);
erasedCount.current = 0;
};

Expand Down Expand Up @@ -80,6 +82,7 @@ const UseLotteryCanvas = () => {
clearCanvas(context);
isDrawing.current = false;
setIsScratching(false);
setIsClear(true);
}
};

Expand All @@ -106,6 +109,7 @@ const UseLotteryCanvas = () => {
return {
isCanvasVisible,
isScratching,
isClear,
canvasRef,
setIsCanvasVisible,
setIsScratching,
Expand Down
Loading

0 comments on commit c3e87fd

Please sign in to comment.