-
Notifications
You must be signed in to change notification settings - Fork 1
Feat: goolgle login 페이지 추가 및 연결 #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ca7a600
b747e01
d71dfb2
da7dde3
fc71ef3
3ec770d
8e7eae0
8b29d4b
957f124
8b7e9e1
f6c2c49
4363251
f80ebd2
110754a
8e77caf
38d302f
3fec7cc
48e386a
b87b0cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import apiRequest from '@shared/apis/setting/axiosInstance'; | ||
| import LoadingChippi from '@shared/components/loadingChippi/LoadingChippi'; | ||
| import { useEffect } from 'react'; | ||
| import { useNavigate, useSearchParams } from 'react-router-dom'; | ||
|
|
||
| const GoogleCallback = () => { | ||
| const navigate = useNavigate(); | ||
| const [searchParams] = useSearchParams(); | ||
|
|
||
| useEffect(() => { | ||
| const code = searchParams.get('code'); | ||
|
|
||
| if (!code) { | ||
| alert('로그인 실패. 다시 시도해주세요.'); | ||
| navigate('/onboarding?step=SOCIAL_LOGIN'); | ||
| return; | ||
| } | ||
|
|
||
| loginWithCode(code); | ||
| }, []); | ||
|
|
||
| const handleUserLogin = ( | ||
| isUser: boolean, | ||
| accessToken: string | undefined | ||
| ) => { | ||
| if (isUser) { | ||
| if (accessToken) { | ||
| localStorage.setItem('token', accessToken); | ||
| } | ||
| navigate('/'); | ||
| } else { | ||
| navigate('/onboarding?step=ALARM'); | ||
| } | ||
| }; | ||
|
|
||
| const loginWithCode = async (code: string) => { | ||
| try { | ||
| const res = await apiRequest.post('/api/v2/auth/google', { code }); | ||
| const { isUser, userId, email, accessToken } = res.data.data; | ||
|
|
||
| localStorage.setItem('email', email); | ||
| localStorage.setItem('userId', userId); | ||
|
|
||
| handleUserLogin(isUser, accessToken); | ||
| } catch (error) { | ||
| console.error('로그인 오류:', error); | ||
| navigate('/onboarding?step=SOCIAL_LOGIN'); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex h-screen flex-col items-center justify-center"> | ||
| <LoadingChippi className="mb-6" /> | ||
| <p className="text-font-black-2 head3 mt-[1.6rem]"> | ||
| 잠시만 기다려주세요… | ||
| </p> | ||
| <p className="body1-m text-font-gray-3 text-center"> | ||
| 치삐가 로그인 중입니다 | ||
| </p> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default GoogleCallback; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,28 @@ | ||
| import { Progress, Button } from '@pinback/design-system/ui'; | ||
| import { useState, useEffect, lazy, Suspense } from 'react'; | ||
| import { motion, AnimatePresence } from 'framer-motion'; | ||
| import SocialLoginStep from './step/SocialLoginStep'; | ||
| const StoryStep = lazy(() => import('./step/StoryStep')); | ||
| const AlarmStep = lazy(() => import('./step/AlarmStep')); | ||
| const MacStep = lazy(() => import('./step/MacStep')); | ||
| const FinalStep = lazy(() => import('./step/FinalStep')); | ||
|
|
||
| import { cva } from 'class-variance-authority'; | ||
| import { usePostSignUp } from '@shared/apis/queries'; | ||
| const stepProgress = [{ progress: 30 }, { progress: 60 }, { progress: 100 }]; | ||
| import { AlarmsType } from '@constants/alarms'; | ||
| import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime'; | ||
| import { useNavigate, useLocation } from 'react-router-dom'; | ||
| import { firebaseConfig } from '../../../../firebase-config'; | ||
| import { initializeApp } from 'firebase/app'; | ||
| import { getMessaging, getToken } from 'firebase/messaging'; | ||
| import { registerServiceWorker } from '@pages/onBoarding/utils/registerServiceWorker'; | ||
| import { useLocation } from 'react-router-dom'; | ||
| import { AlarmsType } from '@constants/alarms'; | ||
| import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime'; | ||
| const stepProgress = [{ progress: 33 }, { progress: 66 }, { progress: 100 }]; | ||
| import { | ||
| Step, | ||
| stepOrder, | ||
| StepType, | ||
| storySteps, | ||
| } from '@pages/onBoarding/constants/onboardingSteps'; | ||
|
Comment on lines
+16
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx | head -40Repository: Pinback-Team/pinback-client Length of output: 1931 🏁 Script executed: fd tsconfig.json eslintrc -o json 2>/dev/null | head -20Repository: Pinback-Team/pinback-client Length of output: 53 🏁 Script executed: rg "stepProgress" apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx -B 5 -A 2Repository: Pinback-Team/pinback-client Length of output: 746 🏁 Script executed: find . -name "tsconfig.json" -o -name ".eslintrc*" -o -name "eslint.config.*" | head -10Repository: Pinback-Team/pinback-client Length of output: 337 🏁 Script executed: cat tsconfig.json 2>/dev/null || cat apps/client/tsconfig.json 2>/dev/nullRepository: Pinback-Team/pinback-client Length of output: 539 🏁 Script executed: rg "import/order|sort-imports" --type json --type yamlRepository: Pinback-Team/pinback-client Length of output: 1148 🏁 Script executed: cat apps/client/eslint.config.js | head -100Repository: Pinback-Team/pinback-client Length of output: 288 🏁 Script executed: rg "import-order|no-mixed-require|imports-on-top" apps/client/eslint.config.jsRepository: Pinback-Team/pinback-client Length of output: 53 🏁 Script executed: find . -path "*/eslint-config*" -name "*.js" -o -name "*.json" | grep -v node_modules | head -20Repository: Pinback-Team/pinback-client Length of output: 733 🏁 Script executed: rg "@pinback/eslint-config" --type json --type js | head -5Repository: Pinback-Team/pinback-client Length of output: 453 🏁 Script executed: python3 << 'EOF'
# According to ECMAScript specification (ES2015+):
# - All import/export statements must come before other module code
# - Mixing const declarations between imports violates module structure
# - This is enforced by strict ES module parsers
# Check if the pattern would cause issues:
pattern_lines = """
import { AlarmsType } from '@constants/alarms';
import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime';
const stepProgress = [{ progress: 33 }, { progress: 66 }, { progress: 100 }];
import {
Step,
stepOrder,
StepType,
storySteps,
} from '@pages/onBoarding/constants/onboardingSteps';
"""
import_count_before_const = 2 # Lines 16-17
const_statement = 1 # Line 18
import_count_after_const = 1 # Lines 19-24
if import_count_after_const > 0 and const_statement > 0:
print("VIOLATION: Imports found after non-import code (const declaration)")
print("ES Module Specification: All imports MUST come before other code")
print("This violates: https://tc39.es/ecma262/#sec-parse-script")
print("\nResult: This pattern WILL cause build/parse errors in ES module mode")
else:
print("No violation")
EOFRepository: Pinback-Team/pinback-client Length of output: 157 🏁 Script executed: cat > /tmp/test-module.ts << 'EOF'
import { A } from 'module-a';
import { B } from 'module-b';
const x = [1, 2, 3];
import { C } from 'module-c';
EOF
# Check TypeScript parsing
cat /tmp/test-module.tsRepository: Pinback-Team/pinback-client Length of output: 181 🏁 Script executed: node << 'EOF'
// Check if Node.js would accept this pattern
const moduleCode = `
import { A } from 'module-a';
import { B } from 'module-b';
const x = [1, 2, 3];
import { C } from 'module-c';
`;
console.log("ES Module Specification (ECMAScript 2015):");
console.log("- All imports MUST appear before non-import code at module level");
console.log("- Placing import statements after other code violates the spec");
console.log("");
console.log("Pattern in the file violates this rule:");
console.log("✗ const between imports");
console.log("");
console.log("This WILL cause parse errors in strict ES module mode");
console.log("Build tools like Vite, TypeScript, and bundlers enforce this strictly");
EOFRepository: Pinback-Team/pinback-client Length of output: 431 Move all ES modules require all imports to come before other module-level code. Currently, Reorganize to: import { AlarmsType } from '@constants/alarms';
import { normalizeTime } from '@pages/onBoarding/utils/formatRemindTime';
import {
Step,
stepOrder,
StepType,
storySteps,
} from '@pages/onBoarding/constants/onboardingSteps';
const stepProgress = [{ progress: 33 }, { progress: 66 }, { progress: 100 }];🤖 Prompt for AI Agents |
||
|
|
||
| const variants = { | ||
| slideIn: (direction: number) => ({ | ||
| x: direction > 0 ? 200 : -200, | ||
|
|
@@ -27,6 +34,7 @@ const variants = { | |
| opacity: 0, | ||
| }), | ||
| }; | ||
|
|
||
| const CardStyle = cva( | ||
| 'bg-white-bg flex h-[54.8rem] w-[63.2rem] flex-col items-center justify-between rounded-[2.4rem] pt-[3.2rem]', | ||
| { | ||
|
|
@@ -39,36 +47,41 @@ const CardStyle = cva( | |
| defaultVariants: { overflow: false }, | ||
| } | ||
| ); | ||
|
|
||
| const MainCard = () => { | ||
| const [step, setStep] = useState(0); | ||
| const navigate = useNavigate(); | ||
| const location = useLocation(); | ||
| const { mutate: postSignData } = usePostSignUp(); | ||
|
|
||
| const [step, setStep] = useState<StepType>(Step.STORY_0); | ||
| const [direction, setDirection] = useState(0); | ||
| const [alarmSelected, setAlarmSelected] = useState<1 | 2 | 3>(1); | ||
| const [isMac, setIsMac] = useState(false); | ||
| // api 구간 | ||
| const { mutate: postSignData } = usePostSignUp(); | ||
|
|
||
| // 익스텐션에서부터 이메일 받아오는 구간! | ||
| const [userEmail, setUserEmail] = useState(''); | ||
| const location = useLocation(); | ||
| const [remindTime, setRemindTime] = useState('09:00'); | ||
| const [fcmToken, setFcmToken] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const params = new URLSearchParams(location.search); | ||
| const emailParam = params.get('email'); | ||
| if (emailParam) { | ||
| setUserEmail(emailParam); | ||
| localStorage.setItem('email', emailParam); | ||
| const storedEmail = localStorage.getItem('email'); | ||
| if (storedEmail) { | ||
| setUserEmail(storedEmail); | ||
| } | ||
|
|
||
| const stepParam = params.get('step') as StepType; | ||
| if (stepParam && Object.values(Step).includes(stepParam)) { | ||
| setStep(stepParam); | ||
| } | ||
| }, [location.search]); | ||
|
|
||
| // FCM 구간 | ||
| const [fcmToken, setFcmToken] = useState<string | null>(null); | ||
| const app = initializeApp(firebaseConfig); | ||
| const messaging = getMessaging(app); | ||
|
|
||
|
Comment on lines
77
to
79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Short answer — prevent duplicate initialization by checking for an existing App before calling initializeApp(). Two common patterns for v9 (modular):
if (!getApps().length) {
let app Both avoid the "Firebase App named '[DEFAULT]' already exists (app/duplicate-app)" error. See Firebase docs and examples. [1][2] Sources: 🌐 Web query:
💡 Result: Short answer: initialize Firebase once in module scope (a single firebase.js / firebase.ts file), export the app and needed service instances, and import those where needed — or provide them via React Context. This avoids duplicate apps, keeps tree-shaking with the modular SDK, and makes testing/config clearer. [1][2][3][4] Recommended pattern (modular SDK v9+):
Minimal example (TypeScript/ESM, cite docs): const firebaseConfig = { /* your config */ }; const app = !getApps().length ? initializeApp(firebaseConfig) : getApp(); Why this is best:
If you want, I can show a React Context/provider wrapper and a small test-mocking pattern. Firebase
Recommended fix:
// firebase.ts or similar, at file top level
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getMessaging } from 'firebase/messaging';
const firebaseConfig = { /* ... */ };
// Guard against duplicate initialization
const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
export const messaging = getMessaging(app);
import { messaging } from '@/firebase'; // or wherever you define it
// Inside MainCard, remove these lines:
// const app = initializeApp(firebaseConfig);
// const messaging = getMessaging(app);
// Use the imported messaging directly:
const requestFCMToken = async (): Promise<string | null> => {
const forFcmtoken = await getToken(messaging, {
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
});
// ...
};This ensures Firebase initializes only once, regardless of how many times 🤖 Prompt for AI Agents |
||
| const requestFCMToken = async (): Promise<string | null> => { | ||
| try { | ||
| const permission = await Notification.requestPermission(); | ||
| registerServiceWorker(); | ||
|
|
||
| if (permission !== 'granted') { | ||
| alert('알림 권한 허용이 필요합니다!'); | ||
| return null; | ||
|
|
@@ -107,80 +120,92 @@ const MainCard = () => { | |
| } | ||
| })(); | ||
| }, []); | ||
|
|
||
| const renderStep = () => { | ||
| switch (step) { | ||
| case 0: | ||
| case 1: | ||
| case 2: | ||
| return <StoryStep step={step as 0 | 1 | 2} />; | ||
| case 3: | ||
| case Step.STORY_0: | ||
| case Step.STORY_1: | ||
| case Step.STORY_2: | ||
| return ( | ||
| <StoryStep step={Number(step.replace('STORY_', '')) as 0 | 1 | 2} /> | ||
| ); | ||
| case Step.SOCIAL_LOGIN: | ||
| return <SocialLoginStep />; | ||
| case Step.ALARM: | ||
| return ( | ||
| <AlarmStep selected={alarmSelected} setSelected={setAlarmSelected} /> | ||
| ); | ||
| case 4: | ||
| if (isMac) return <MacStep />; | ||
| case Step.MAC: | ||
| return <MacStep />; | ||
| case Step.FINAL: | ||
| return <FinalStep />; | ||
| case 5: | ||
| if (isMac) return <FinalStep />; | ||
| return null; | ||
| default: | ||
| return <FinalStep />; | ||
| } | ||
| }; | ||
|
|
||
| const [remindTime, setRemindTime] = useState('09:00'); | ||
| const nextStep = async () => { | ||
| if (step === 3) { | ||
| if (alarmSelected == 1) { | ||
| setRemindTime('09:00'); | ||
| } else if (alarmSelected == 2) { | ||
| setRemindTime('20:00'); | ||
| } else { | ||
| const idx = stepOrder.indexOf(step); | ||
| const next = stepOrder[idx + 1]; | ||
| const isAlarmStep = step === Step.ALARM; | ||
| const isFinalStep = step === Step.FINAL; | ||
| const isMacStep = next === Step.MAC; | ||
| const shouldSkipMacStep = isMacStep && !isMac; | ||
|
|
||
| if (isAlarmStep) { | ||
| if (alarmSelected === 1) setRemindTime('09:00'); | ||
| else if (alarmSelected === 2) setRemindTime('20:00'); | ||
| else { | ||
| const raw = AlarmsType[alarmSelected - 1].time; | ||
| setRemindTime(normalizeTime(raw)); | ||
| } | ||
| } | ||
|
|
||
| if (shouldSkipMacStep) { | ||
| setDirection(1); | ||
| setStep((prev) => prev + 1); | ||
| setStep(Step.FINAL); | ||
| navigate(`/onboarding?step=${Step.FINAL}`); | ||
| return; | ||
| } | ||
| if ((isMac && step < 5) || (!isMac && step < 4)) { | ||
| setDirection(1); | ||
| setStep((prev) => prev + 1); | ||
| } else if ((isMac && step === 5) || (!isMac && step == 4)) { | ||
|
|
||
| if (isFinalStep) { | ||
| postSignData( | ||
| { email: userEmail, remindDefault: remindTime, fcmToken }, | ||
| { | ||
| email: userEmail, | ||
| remindDefault: remindTime, | ||
| fcmToken: fcmToken, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| window.location.href = '/'; | ||
| }, | ||
| onSuccess: () => (window.location.href = '/'), | ||
| onError: () => { | ||
| const savedEmail = localStorage.getItem('email'); | ||
| if (savedEmail) { | ||
| window.location.href = '/'; | ||
| } | ||
| if (savedEmail) window.location.href = '/'; | ||
| }, | ||
| } | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| setDirection(1); | ||
| setStep(next); | ||
| navigate(`/onboarding?step=${next}`); | ||
| }; | ||
|
|
||
| const prevStep = () => { | ||
| if (step > 0) { | ||
| const idx = stepOrder.indexOf(step); | ||
| if (idx > 0) { | ||
| const previous = stepOrder[idx - 1]; | ||
| setDirection(-1); | ||
| setStep((prev) => prev - 1); | ||
| setStep(previous); | ||
| navigate(`/onboarding?step=${previous}`); | ||
| } | ||
| }; | ||
|
Comment on lines
190
to
198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 비Mac 사용자가 뒤로가기 시 MAC 단계에 도달할 수 있습니다.
const prevStep = () => {
const idx = stepOrder.indexOf(step);
if (idx > 0) {
- const previous = stepOrder[idx - 1];
+ let previous = stepOrder[idx - 1];
+ // 비Mac 사용자가 MAC 단계를 건너뛰도록 처리
+ if (previous === Step.MAC && !isMac) {
+ previous = Step.ALARM;
+ }
setDirection(-1);
setStep(previous);
navigate(`/onboarding?step=${previous}`);
}
};🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div className={CardStyle({ overflow: step === 3 && alarmSelected === 3 })}> | ||
| {step < 3 && ( | ||
| <div | ||
| className={CardStyle({ | ||
| overflow: step === Step.ALARM && alarmSelected === 3, | ||
| })} | ||
| > | ||
| {storySteps.includes(step) && ( | ||
| <Progress | ||
| value={stepProgress[step].progress} | ||
| value={stepProgress[storySteps.indexOf(step)].progress} | ||
| variant="profile" | ||
| className="w-[15.6rem]" | ||
| /> | ||
|
|
@@ -204,26 +229,27 @@ const MainCard = () => { | |
| </div> | ||
|
|
||
| <div className="mb-[4.8rem] mt-[1.2rem] flex w-full justify-between px-[3.2rem]"> | ||
| {step < 4 && step > 0 && ( | ||
| {!([Step.STORY_0, Step.SOCIAL_LOGIN] as StepType[]).includes(step) && ( | ||
| <Button | ||
| variant="secondary" | ||
| size="medium" | ||
| isDisabled={step === 0} | ||
| className="w-[4.8rem]" | ||
| onClick={prevStep} | ||
| > | ||
| 뒤로 | ||
| </Button> | ||
| )} | ||
| <Button | ||
| variant="primary" | ||
| size="medium" | ||
| isDisabled={step === 6} | ||
| className="ml-auto w-[4.8rem]" | ||
| onClick={nextStep} | ||
| > | ||
| 다음 | ||
| </Button> | ||
|
|
||
| {step !== Step.SOCIAL_LOGIN && ( | ||
| <Button | ||
| variant="primary" | ||
| size="medium" | ||
| className="ml-auto w-[4.8rem]" | ||
| onClick={nextStep} | ||
| > | ||
| 다음 | ||
| </Button> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import Chippi from '@assets/chippi_extension_popup.svg'; | ||
| import GoogleLogo from '/assets/onBoarding/icons/googleLogo.svg'; | ||
|
|
||
| const SocialLoginStep = () => { | ||
| const handleGoogleLogin = () => { | ||
| const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID; | ||
| const redirectUri = import.meta.env.VITE_GOOGLE_REDIRECT_URI; | ||
|
|
||
| if (!clientId || !redirectUri) { | ||
| alert('Google OAuth 설정이 누락되었습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| const googleAuthUrl = | ||
| `https://accounts.google.com/o/oauth2/v2/auth?` + | ||
| `client_id=${clientId}` + | ||
| `&redirect_uri=${redirectUri}` + | ||
| `&response_type=code` + | ||
| `&scope=email profile`; | ||
|
|
||
| window.location.href = googleAuthUrl; | ||
| }; | ||
|
Comment on lines
+4
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainGoogle OAuth URL 구성 시 보안(state) 및 인코딩 개선 필요 현재
예시는 아래처럼 구현할 수 있습니다. - const googleAuthUrl =
- `https://accounts.google.com/o/oauth2/v2/auth?` +
- `client_id=${clientId}` +
- `&redirect_uri=${redirectUri}` +
- `&response_type=code` +
- `&scope=email profile`;
-
- window.location.href = googleAuthUrl;
+ const state = crypto.randomUUID();
+ sessionStorage.setItem('google_oauth_state', state);
+
+ const params = new URLSearchParams({
+ client_id: clientId,
+ redirect_uri: redirectUri,
+ response_type: 'code',
+ scope: 'email profile',
+ state,
+ });
+
+ window.location.href =
+ `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;(콜백 쪽에서 Also applies to: 40-50 🌐 Web query: 💡 Result: 요약 — 목적과 권장 패턴
참고(주요 출처)
원하시면 샘플 구현 절차(요청 예시, state/PKCE 생성·검증 코드 스니펫, 안전한 토큰 저장 방안)를 한국어 코드 예제로 바로 드리겠습니다. 웹 검색 결과를 검토했습니다. Google OAuth 공식 문서와 IETF 권고에서 확인된 내용: ✓ state 파라미터는 필수 — CSRF 공격 방지 및 요청-응답 매칭 검증용 원본 리뷰의 보안 지적이 모두 검증되었습니다. 최종 리뷰 코멘트를 생성하겠습니다. Google OAuth URL 구성 시 CSRF 방지(state) 및 보안 개선 필수 현재
구현 예시: - const googleAuthUrl =
- `https://accounts.google.com/o/oauth2/v2/auth?` +
- `client_id=${clientId}` +
- `&redirect_uri=${redirectUri}` +
- `&response_type=code` +
- `&scope=email profile`;
-
- window.location.href = googleAuthUrl;
+ const state = crypto.randomUUID();
+ sessionStorage.setItem('google_oauth_state', state);
+
+ const params = new URLSearchParams({
+ client_id: clientId,
+ redirect_uri: redirectUri,
+ response_type: 'code',
+ scope: 'email profile',
+ state,
+ });
+
+ window.location.href =
+ `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;콜백 컴포넌트에서도 (Also applies to: lines 40-50) 🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center justify-center pt-6"> | ||
| <img | ||
| src={Chippi} | ||
| alt="치삐 이미지" | ||
| className="h-[19.4rem] w-[19.4rem] object-contain" | ||
| /> | ||
|
|
||
| <h1 className="head2 text-font-black-1 mb-[0.8rem] text-center"> | ||
| 치삐를 만나려면 로그인이 필요해요! | ||
| </h1> | ||
|
|
||
| <p className="body2-m text-font-gray-3 mb-[3.5rem] text-center"> | ||
| 로그인하고 북마크한 정보를 리마인드를 받아보세요. | ||
| </p> | ||
|
|
||
| <button | ||
| onClick={handleGoogleLogin} | ||
| className="sub2-sb flex h-[5.2rem] w-[22.7rem] items-center justify-between gap-3 rounded-full border border-gray-100 bg-white px-[2rem]" | ||
| > | ||
| <img | ||
| src={GoogleLogo} | ||
| alt="구글 로고" | ||
| className="h-[2.435rem] w-[2.435rem]" | ||
| /> | ||
| 구글 계정으로 로그인 | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default SocialLoginStep; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect 의존성 배열에 누락된 항목이 있습니다.
useEffect내부에서navigate와loginWithCode를 사용하고 있지만 의존성 배열에 포함되지 않았습니다. React 19에서는 이를 경고하며, 예상치 못한 동작이 발생할 수 있습니다.다음과 같이 수정하세요:
useEffect(() => { const params = new URLSearchParams(window.location.search); const code = params.get('code'); if (!code) { alert('로그인 실패. 다시 시도해주세요.'); navigate('/onboarding?step=3'); return; } loginWithCode(code); - }, []); + }, [navigate, loginWithCode]);단,
loginWithCode를 의존성에 추가하면 매 렌더링마다 재생성되므로 무한 루프가 발생할 수 있습니다.loginWithCode를useCallback으로 감싸거나, useEffect 내부로 이동시키는 것을 권장합니다.🤖 Prompt for AI Agents