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
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import PlayerProfilePage from "./pages/playerProfile/PlayerProfilePage";

// 프로필(MyPage)
import MyPage from "./pages/Profile/MyPage";
import Profile from "./pages/Profile/components/Profile";
import Info from "./pages/Profile/components/Info";
import ProfileEdit from "./pages/Profile/components/ProfileEdit";
import Profile from "./pages/Profile/Profile";
import Info from "./pages/Profile/Info";
import ProfileEdit from "./pages/Profile/ProfileEdit";

// 매칭
import MatchingGameListPage from "./pages/Matching/MatchingGameListPage";
Expand Down
31 changes: 30 additions & 1 deletion src/api/ai/apis.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import axiosInstance from "../axiosInstance";
import { AIPredictionResponse } from "../../types/ai";
import {
AIPredictionResponse,
FinalRankingResponse,
GamePredictionResponse,
} from "../../types/ai";

export const aiApi = {
getWinProbability: async (date: string): Promise<AIPredictionResponse> => {
Expand All @@ -12,4 +16,29 @@ export const aiApi = {
winProbability: item.winProbability,
}));
},
getFinalRank: async (): Promise<FinalRankingResponse> => {
const { data: raw } =
await axiosInstance.get<FinalRankingResponse>("/final-rankings");
return raw.map((item: any) => ({
id: item.id,
teamName: item.teamName,
rank: item.rank,
currentRank: item.currentRank,
}));
},
getGamePrediction: async (): Promise<GamePredictionResponse> => {
const { data: raw } = await axiosInstance.get<GamePredictionResponse>(
"/api/win-rates/live-prediction",
);
return raw.map((item: any) => ({
gameId: item.gameId,
awayTeam: item.awayTeam,
homeTeam: item.homeTeam,
inning: item.inning,
winProbability: item.winProbability,
homeAccumScore: item.homeAccumScore,
awayAccumScore: item.awayAccumScore,
predictedAt: item.predictedAt,
}));
},
};
56 changes: 52 additions & 4 deletions src/api/ranking/apis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
PitcherSaveDto,
PitcherStrikeoutDto,
PitcherWinRateDto,
PlayerAVGDto,
PlayerHitDto,
PlayerHRDto,
PlayerRBIDto,
RankingDto,
} from "../../types/ranking";
import {
Expand Down Expand Up @@ -41,7 +45,7 @@ export const rankingApi = {
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
wpct: item.wpct,
wpct: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
Expand All @@ -52,7 +56,7 @@ export const rankingApi = {
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
sv: item.sv,
sv: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
Expand All @@ -63,7 +67,7 @@ export const rankingApi = {
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
so: item.so,
so: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
Expand All @@ -74,7 +78,51 @@ export const rankingApi = {
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
era: item.era,
era: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
getRankPlayerHR: async () => {
const response = await axiosInstance.get<PlayerHRDto>(
"/record/personalRank/hitter/HR",
);
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
hr: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
getRankPlayerRBI: async () => {
const response = await axiosInstance.get<PlayerRBIDto>(
"/record/personalRank/hitter/rbi",
);
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
rbi: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
getRankPlayerAVG: async () => {
const response = await axiosInstance.get<PlayerAVGDto>(
"/record/personalRank/hitter/avg",
);
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
avg: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
getRankPlayerHit: async () => {
const response = await axiosInstance.get<PlayerHitDto>(
"/record/personalRank/hitter/H",
);
return response.data.data.map(item => ({
playerName: item.playerName,
backNumber: item.backNumber,
hit: item.value,
playerImageUrl: item.playerImageUrl,
}));
},
Expand Down
4 changes: 4 additions & 0 deletions src/pages/Home/components/KoreaMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ export default function KoreaMap({
)}
</a>
</div>
{/* 데이터 출처 */}
<div className="absolute right-0 bottom-0 text-[10px] text-[var(--on-surface-grey2)]">
(공공데이터 포털 한국환경공단 데이터를 사용 중입니다.)
</div>
</div>
</div>
);
Expand Down
79 changes: 42 additions & 37 deletions src/pages/Login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,37 @@ interface KakaoUserInfoResponse {
success: boolean;
message: string;
data: {
accessToken: string;
accessToken: string; // 백엔드가 내려주는 JWT 토큰
tokenType: string;
expiresIn: number;
id?: number;
has_signed_up?: boolean;
properties?: { nickname: string };
};
}

const LoginPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();

// 이미 jwtToken이 있으면 /login(id === undefined)이 아니라 바로 홈으로
// 이미 jwtToken이 있으면 id(param)가 undefined인 순수 로그인 화면이 아니라 곧바로 홈으로 리다이렉트
useEffect(() => {
const token = localStorage.getItem("jwtToken");
if (!id && token) {
window.location.href = "/";
}
}, [id, navigate]);
}, [id]);

// 카카오 SDK 초기화
// 카카오 SDK 초기화 (페이지 로드 시 한 번만 수행)
useEffect(() => {
if (window.Kakao && !window.Kakao.isInitialized()) {
window.Kakao.init(import.meta.env.VITE_KAKAO_JS_KEY);
}
}, []);

// 회원가입 1단계(닉네임 입력)과 2단계(팀 선택)를 위한 상태
const [nickname, setNickname] = useState<string>("");
const [nicknameCheckResult, setNicknameCheckResult] = useState<
"available" | "duplicate" | null
>(null);
const [nicknameCheckResult, setNicknameCheckResult] = useState<"available" | "duplicate" | null>(null);
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);

// 카카오 로그인 버튼 클릭 시 실행될 함수
const kakaoLogin = () => {
if (!window.Kakao) {
return alert("카카오 SDK가 로드되지 않았습니다.");
Expand All @@ -58,26 +55,27 @@ const LoginPage: React.FC = () => {
throughTalk: false,
success: async (authObj: any) => {
try {
// 1) 카카오 accessToken으로 백엔드 로그인
// 1) 카카오 access_token을 백엔드로 전달하여 우리 서버에서 JWT 발급받기
const res = await fetch(
`${import.meta.env.VITE_API_URL}/api/kakao/user-info`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accessToken: authObj.access_token }),
},
}
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}

// 2) 백엔드가 내려준 JSON 파싱
const resp = (await res.json()) as KakaoUserInfoResponse;

// 2) JWT 저장
// 3) JWT를 로컬스토리지에 저장
const jwt = resp.data.accessToken;
localStorage.setItem("jwtToken", jwt);
if (resp.data.id !== undefined) {
localStorage.setItem("userIdx", String(resp.data.id));
}

// 3) 가입 여부 조회 (/login/hasSignedIn)
// 4) 가입 여부 조회 (hasSignedIn)
const hasRes = await fetch(
`${import.meta.env.VITE_API_URL}/login/hasSignedIn`,
{
Expand All @@ -86,25 +84,24 @@ const LoginPage: React.FC = () => {
"Content-Type": "application/json",
Authorization: `Bearer ${jwt}`,
},
},
}
);
if (!hasRes.ok)
if (!hasRes.ok) {
throw new Error(`hasSignedIn 호출 실패: ${hasRes.status}`);
const hasJson = (await hasRes.json()) as {
}

const hasJson = await hasRes.json() as {
success: boolean;
message: string;
data: { hasSignedIn: boolean };
};

// 4) 분기 처리
// 5) 분기 처리
if (hasJson.data.hasSignedIn) {
// 이미 모두 완료된 회원: 홈으로
// 이미 가입된 회원: 홈으로 이동
window.location.href = "/";
} else {
// 닉네임/응원팀 설정 필요
if (resp.data.properties?.nickname) {
setNickname(resp.data.properties.nickname);
}
// 신규 회원: 닉네임/응원팀 설정 단계로 이동
navigate("/signup/1");
}
} catch (err) {
Expand All @@ -119,30 +116,37 @@ const LoginPage: React.FC = () => {
});
};

// 회원 가입 단계별 “다음” 버튼 핸들러
const goStep2 = () => {
if (nicknameCheckResult === "available") navigate("/signup/2");
else alert("닉네임 중복 체크를 완료해주세요!");
if (nicknameCheckResult === "available") {
navigate("/signup/2");
} else {
alert("닉네임 중복 체크를 완료해주세요!");
}
};

const goStep3 = () => {
if (selectedTeam) navigate("/signup/3");
else alert("응원팀을 선택해주세요!");
if (selectedTeam) {
navigate("/signup/3");
} else {
alert("응원팀을 선택해주세요!");
}
};

// id 파라미터 없으면 순수 로그인 화면
// id 파라미터가 없으면 순수 로그인 화면을 렌더링
if (!id) {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100 px-4">
<div className="mx-auto w-full max-w-md text-center">
<p className="mb-1 text-gray-600">야구를 더 가까이, 더 즐겁게</p>
<h1 className="text-navy-800 mb-8 text-4xl font-extrabold">DUGOUT</h1>

<div className="mb-6 flex w-full items-center">
<div className="h-px flex-1 bg-gray-300" />
<span className="px-4 text-sm text-gray-500">
SNS로 간편 로그인
</span>
<span className="px-4 text-sm text-gray-500">SNS로 간편 로그인</span>
<div className="h-px flex-1 bg-gray-300" />
</div>

<div className="w-full space-y-4">
<button
onClick={kakaoLogin}
Expand Down Expand Up @@ -184,6 +188,7 @@ const LoginPage: React.FC = () => {
</span>
</button>
</div>

<p className="mt-6 px-2 text-center text-sm text-gray-500">
계속 진행 시{" "}
<button
Expand All @@ -206,7 +211,7 @@ const LoginPage: React.FC = () => {
);
}

// signup 스텝별 렌더링
// signup 스텝(id === "1", "2", "3")에 따라 각각의 컴포넌트를 렌더링
switch (id) {
case "1":
return (
Expand All @@ -223,7 +228,7 @@ const LoginPage: React.FC = () => {
<TeamSelection
selectedTeam={selectedTeam}
setSelectedTeam={setSelectedTeam}
onNext={goStep3}
onNext={goStep3} // ← 여기서 goStep3를 넘겨줍니다
/>
);
case "3":
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Login/components/NickNameInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const NicknameInput: React.FC<NicknameInputProps> = ({
console.log('Nickname to check:', nickname);
console.log('Using token:', token);

const res = await fetch('/login/nickname', {
const res = await fetch(`${import.meta.env.VITE_API_URL}/login/nickname`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Loading