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
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import SigninScreen from './pages/Auth/SigninScreen';
import CreateAccountPage from './pages/Auth/CreateAccountPage';
import TermsOfServicePage from './pages/Auth/TermsOfServicePage';
import CreateProfilePage from './pages/Auth/CreateProfilePage';
import CreateDefaultProfilePage from './pages/Auth/CreateDefaultProfilePage';
import SelectCharacterPage from './pages/Auth/SelectCharacterPage';
import AccountCreatedPage from './pages/Auth/AccountCreatedPage';
import KakaoCallback from './pages/Auth/KakaoCallback';


import SettingsHomePage from './pages/settings-page/SettingHomepage';
import ProfileEditPage from './pages/settings-page/ProfileEditpage';
import NotificationSettingsPage from './pages/settings-page/NotificationSettingsPage';
Expand Down Expand Up @@ -95,6 +97,7 @@ const publicRoutes: RouteObject[] = [
{ path: 'signup', element: <CreateAccountPage /> },
{ path: 'terms', element: <TermsOfServicePage /> },
{ path: 'create-profile', element: <CreateProfilePage /> },
{ path: 'create', element: <CreateDefaultProfilePage /> },
{ path: 'select', element: <SelectCharacterPage /> },
{ path: 'account', element: <AccountCreatedPage /> },
{ path: 'auth/kakao/callback', element: <KakaoCallback /> },
Expand Down
3 changes: 2 additions & 1 deletion src/pages/Auth/CreateAccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function CreateAccountPage() {

const [agree, setAgree] = useState(false);


/* ---------------------- 비밀번호 검증 ---------------------- */
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
Expand Down Expand Up @@ -129,7 +130,7 @@ export default function CreateAccountPage() {
birthDate,
};

navigate("/create-profile", { state: allAccountData });
navigate("/create", { state: allAccountData });
};

/* ----------------------------- UI ----------------------------- */
Expand Down
193 changes: 193 additions & 0 deletions src/pages/Auth/CreateDefaultProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { checkNickname } from '../../api/auth';

export default function CreateProfilePage() {
const navigate = useNavigate();

const [nickname, setNickname] = useState('');
const [introduction, setIntroduction] = useState('');
const [link, setLink] = useState('');

const [nicknameAvailable, setNicknameAvailable] = useState<boolean | null>(null);
const [checkingNickname, setCheckingNickname] = useState(false);

const maxIntroLength = 100;


/* ------------------- 닉네임 자동 확인 (API 없어도 통과) ------------------- */
useEffect(() => {
if (!nickname.trim()) {
setNicknameAvailable(null); // 닉네임 비어있으면 메시지 숨기기
return;
}

const timeout = setTimeout(async () => {
setCheckingNickname(true);

try {
const res = await checkNickname(nickname);
setNicknameAvailable(res.available);
} catch (error) {
// API 없어도 통과되도록
console.warn("닉네임 확인 실패 → 사용 가능으로 처리");
setNicknameAvailable(true);
}

setCheckingNickname(false);
}, 500);

return () => clearTimeout(timeout);
}, [nickname]);


/* ------------------------ 저장 ------------------------ */
const handleSubmit = async () => {
if (!nickname.trim()) return alert('닉네임을 입력해주세요.');
if (checkingNickname) return alert('닉네임 확인 중입니다.');

const profilePayload = {
nickname,
introduction,
link,
};

console.log("저장할 프로필 데이터:", profilePayload);

alert("프로필 저장 완료!");
navigate("/select");
};


return (
<div className="flex flex-col items-center justify-start min-h-screen bg-white px-[31px] pt-[118px] relative">

{/* 뒤로가기 */}
<img
src="/images/109618.png"
alt="뒤로가기"
className="absolute top-[25px] left-[30px] w-[35px] h-[35px] cursor-pointer"
onClick={() => navigate(-1)}
/>

{/* 타이틀 */}
<h1 className="text-[29px] font-semibold mb-6 text-center">
프로필을 생성하세요
</h1>

{/* 프로필 이미지 */}
<div className="relative w-[120px] h-[120px] mb-4">
<div className="w-full h-full bg-[#EADCDC] rounded-full flex items-center justify-center">
<img
src="/images/profile.png"
alt="프로필"
className="w-[120px] h-[120px] opacity-70"
/>
</div>
<div className="absolute bottom-0 right-0 bg-white w-[30px] h-[30px] rounded-full shadow flex items-center justify-center">
<img
src="/images/edit-icon.png"
alt="프로필 수정"
className="w-[15px] h-[15px]"
/>
</div>
</div>

{/* 닉네임 영역 */}
<div className="flex flex-col items-center mt-2 mb-6">
<p className="text-[29px] font-semibold text-black">닉네임</p>
<div className="h-[20px]" />
<div className="w-[329px] h-[2px] bg-gray-300" />
</div>

{/* 닉네임 입력 */}
<div className="w-[329px] flex flex-col space-y-1 mb-6">
<label className="text-[15px] font-semibold mb-1 block">닉네임</label>
<div className="relative">
<input
type="text"
placeholder="닉네임"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
className="w-full h-[50px] rounded-2xl border px-10 text-[15px]"
/>
<img
src="/images/user-icon.png"
className="absolute left-3 top-4 w-[18px] h-[18px] opacity-60"
/>
</div>

{/* 닉네임 중복 메시지 */}
{nickname.trim() !== "" && checkingNickname && (
<p className="text-gray-400 text-[13px] mt-1 animate-pulse">
닉네임 확인 중...
</p>
)}

{nickname.trim() !== "" && !checkingNickname && nicknameAvailable === true && (
<p className="text-green-500 text-[13px] mt-1">
사용 가능한 닉네임입니다
</p>
)}

{nickname.trim() !== "" && !checkingNickname && nicknameAvailable === false && (
<p className="text-red-500 text-[13px] mt-1">
이미 사용 중인 닉네임입니다
</p>
)}

</div>

{/* 자기소개 */}
<div className="w-[329px] flex flex-col space-y-1 mb-4">
<label className="text-[15px] font-semibold mb-1 block">자기 소개</label>
<div className="relative">
<textarea
placeholder="자기 소개를 입력해주세요."
value={introduction}
onChange={(e) => setIntroduction(e.target.value.slice(0, maxIntroLength))}
className="w-full h-[90px] rounded-2xl border px-10 py-2 text-[15px] resize-none"
/>
<img
src="/images/pencil-icon.png"
className="absolute left-3 top-3 w-[16px] h-[16px] opacity-60"
/>
<p className="absolute bottom-2 right-3 text-gray-400 text-[12px]">
{introduction.length} / {maxIntroLength}
</p>
</div>
</div>

{/* 외부 링크 */}
<div className="w-[329px] flex flex-col space-y-1">
<label className="text-[15px] font-semibold mb-1 block">외부 링크</label>
<div className="relative">
<input
type="text"
value={link}
placeholder="예: https://instagram.com/..."
onChange={(e) => setLink(e.target.value)}
className="w-full h-[50px] rounded-2xl border px-10 text-[15px]"
/>
<img
src="/images/link-icon.png"
className="absolute left-3 top-4 w-[16px] h-[16px] opacity-60 rotate-45"
/>
</div>
</div>

{/* 저장 버튼 */}
<button
disabled={nickname.trim() === "" || checkingNickname}
onClick={handleSubmit}
className={`w-[329px] h-[60px] text-white text-[18px] font-semibold rounded-2xl mt-8 ${
nickname.trim() !== "" && !checkingNickname
? "bg-[#FF7070]"
: "bg-gray-300 cursor-not-allowed"
}`}
>
프로필 저장
</button>
</div>
);
}
70 changes: 18 additions & 52 deletions src/pages/Auth/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,65 +7,44 @@ export default function LoginScreen() {

useEffect(() => {
if (error) {
console.error("=== 카카오 로그인 에러 발생 ===");
console.error("에러 코드:", error);
console.error("전체 URL:", window.location.href);

// 에러 메시지 매핑
const errorMessages: Record<string, string> = {
"no_token": "인증 토큰을 받지 못했습니다.",
"no_user_id": "사용자 정보를 받지 못했습니다.",
"callback_error": "로그인 처리 중 오류가 발생했습니다.",
};

const message = errorMessages[error] || "카카오 로그인에 실패했습니다. 백엔드 로그를 확인해주세요.";

alert(message);
}
}, [error]);

const handleKakaoLogin = () => {
console.log("=== 카카오 로그인 시작 ===");
console.log("현재 위치:", window.location.href);
console.log("백엔드 OAuth URL로 리다이렉트합니다...");

// localStorage 초기화 (이전 로그인 정보 제거)
localStorage.removeItem("accessToken");
localStorage.removeItem("userId");
localStorage.removeItem("nickname");
localStorage.removeItem("profileImage");

console.log("localStorage 초기화 완료");

const backendOAuthUrl = "https://www.momentory.store/oauth2/authorization/kakao";
console.log("이동할 URL:", backendOAuthUrl);
const backendOAuthUrl =
"https://www.momentory.store/oauth2/authorization/kakao";

window.location.href = backendOAuthUrl;
};

const handleNaverLogin = () => {
console.log("네이버 로그인 - 아직 미구현");
alert("네이버 로그인은 준비 중입니다.");
};

const handleGoogleLogin = () => {
console.log("구글 로그인 - 아직 미구현");
alert("구글 로그인은 준비 중입니다.");
};

return (
<div className="flex flex-col items-center justify-center h-screen bg-white px-6">

<div className="flex flex-col items-center justify-between h-screen bg-white px-6 py-10">
{/* 로고 섹션 */}
<div className="flex flex-col items-center mb-40 mt-40">
<div className="flex flex-col items-center mt-32">
<p
style={{
fontSize: "13px",
fontFamily: "Pretendard",
lineHeight: "100%",
transform: "scale(0.88)",
transformOrigin: "left",
marginBottom: "-15px",
marginLeft: "-125px",
marginBottom: "-12px",
marginLeft: "-90px",
letterSpacing: "-0.2px",
color: "#000",
opacity: 0.8,
Expand All @@ -74,7 +53,7 @@ export default function LoginScreen() {
나만의 경기도 여행 사진첩,
</p>

<div className="w-[282px] h-[85px] flex items-center justify-center scale-[1.7] overflow-hidden">
<div className="w-[260px] h-[80px] flex items-center justify-center scale-[1.6] overflow-hidden">
<img
src="/images/splash-logo.png"
alt="Momentory 로고"
Expand All @@ -83,32 +62,29 @@ export default function LoginScreen() {
</div>
</div>

{/* 로그인 버튼 */}
<div className="mt-40 w-full max-w-[330px]">
{/* 로그인 버튼들 */}
<div className="w-full max-w-[330px] -mt-10">
<Link
to="/signinscreen"
className="w-full h-[70px] bg-[#FF7070] text-white text-[16px] font-medium flex items-center justify-center rounded-[25px] mb-4 shadow-sm transition active:scale-95 hover:bg-[#FF5858]"
className="w-full h-[70px] bg-[#FF7070] text-white text-[16px] font-medium flex items-center justify-center rounded-[25px] mb-4 shadow-sm"
>
로그인
</Link>

<Link
to="/signup"
className="w-full h-[70px] bg-gray-200 text-gray-600 text-[16px] font-medium flex items-center justify-center rounded-[25px] mb-10 shadow-sm transition active:scale-95 hover:bg-gray-300"
className="w-full h-[70px] bg-gray-200 text-gray-600 text-[16px] font-medium flex items-center justify-center rounded-[25px] mb-8 shadow-sm"
>
회원가입
</Link>

<div className="w-full h-[1px] bg-gray-300 mb-6"></div>
<div className="w-full h-[1px] bg-gray-300 mb-6" />

{/* 소셜 로그인 */}
<div className="flex justify-center gap-5">

{/* 카카오 로그인 */}
{/* 소셜 로그인 아이콘 */}
<div className="flex justify-center gap-5 mb-4">
<button
onClick={handleKakaoLogin}
className="w-10 h-10 rounded-full hover:scale-110 transition cursor-pointer focus:outline-none focus:ring-2 focus:ring-yellow-400"
aria-label="카카오 로그인"
className="w-10 h-10 rounded-full hover:scale-110 transition"
>
<img
src="/images/kakao-logo.png"
Expand All @@ -117,25 +93,15 @@ export default function LoginScreen() {
/>
</button>

{/* 네이버 로그인 */}
<button
onClick={handleNaverLogin}
className="w-10 h-10 rounded-full hover:scale-110 transition cursor-pointer focus:outline-none focus:ring-2 focus:ring-green-500"
aria-label="네이버 로그인"
>
<button className="w-10 h-10 rounded-full hover:scale-110 transition">
<img
src="/images/naver-logo.png"
alt="네이버 로그인"
className="w-full h-full rounded-full"
/>
</button>

{/* 구글 로그인 */}
<button
onClick={handleGoogleLogin}
className="w-10 h-10 rounded-full hover:scale-110 transition cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="구글 로그인"
>
<button className="w-10 h-10 rounded-full hover:scale-110 transition">
<img
src="/images/google-logo.png"
alt="구글 로그인"
Expand Down
Loading
Loading