Skip to content
Merged
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
78 changes: 42 additions & 36 deletions src/pages/Login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,42 @@ declare global {
}
}

// Swagger 실제 응답 스키마에 맞춰 data 내부 필드를 최소화합니다.
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 +56,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 파싱 (응답 스키마에 따라 interface를 수정했기 때문에 data 내부에는 accessToken, tokenType, expiresIn만 있음)
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 +85,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 +117,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 +189,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 +212,7 @@ const LoginPage: React.FC = () => {
);
}

// signup 스텝별 렌더링
// signup 스텝(id === "1", "2", "3")에 따라 각각의 컴포넌트를 렌더링
switch (id) {
case "1":
return (
Expand Down