diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33677c4..855c64e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,6 @@ jobs: NEXT_PUBLIC_SUPABASE_URL: "https://dummy.supabase.co" NEXT_PUBLIC_SUPABASE_ANON_KEY: "dummy-key-for-build-test" - # 9. 성공 시 체크마크 출력 - name: All checks passed - run: echo "모든 코드 품질 검사를 통과했습니다!" \ No newline at end of file + run: echo "모든 코드 품질 검사를 통과했습니다!" diff --git a/.github/workflows/devlop.yml b/.github/workflows/devlop.yml deleted file mode 100644 index 6d37717..0000000 --- a/.github/workflows/devlop.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Develop - Integration Test - -on: - push: - branches: - - develop - pull_request: - branches: - - develop - -# 동시 실행 제한 -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - # Develop 환경 변수 (GitHub Secrets 사용) - NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} - NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} - -jobs: - integration-test: - name: Integration Test - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.x] - - steps: - # 1. 코드 체크아웃 - - name: Checkout repository - uses: actions/checkout@v4 - - # 2. pnpm 설정 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - # 3. Node.js 설정 - - name: Setup Node.js ${{ matrix.node-version}} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - # 4. 의존성 설치 - - name: Install dependencies - run: pnpm install --frozen-lockfile - - # 5. 코드 품질 검사 - - name: Code formatting check - run: pnpm run format:check - - - name: Lint check - run: pnpm run lint - - - name: Type check - run: pnpm run type-check - - # 6. 실제 환경변수로 빌드 테스트 - - name: Build for develop - run: pnpm run build - - # # 7. Jest 테스트 (나중에 활성화) - # - name: Run Tests - # run: pnpm run test - - # 8. E2E 테스트 (Playwright - 나중에 활성화) - # - name: E2E Tests - # run: pnpm exec playwright test - - # 9. 빌드 결과물 업로드 (디버깅용) - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - if: failure() - with: - name: build-output - path: .next/ - retention-days: 1 - - # 10. 성공 알림 - - name: Integration test passed - run: echo "Develop 브랜치 통합 테스트 완료!" diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index e98084b..07ac25a 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -106,10 +106,9 @@ jobs: vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} # TODO: 나중에 배포 후 입력 vercel-args: "--prod" - # 배포 성공 알림 (선택사항) - name: Deployment success notification run: | echo "프로덕션 배포 완료!" echo "배포 시간: ${date}" - echo "커밋: ${{ github.sha}}" \ No newline at end of file + echo "커밋: ${{ github.sha}}" diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 28bff16..8e5c9d4 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -1,16 +1,142 @@ -import LoginPageContent from "@/components/auth/LoginPageContent"; -import { Suspense } from "react"; +"use client"; + +import AuthGuard from "@/components/auth/AuthGuard"; +import { useAuth } from "@/hooks/auth"; +import { LoginFormValues, loginSchema } from "@/lib/validations/auth"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; +import { useForm } from "react-hook-form"; + +const LoginPage = () => { + const { login, isLoggingIn, loginError } = useAuth(); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(loginSchema), + mode: "onChange", // 실시간 검증 + }); + + const onSubmit = async (data: LoginFormValues) => { + try { + await login(data); + } catch (error) { + // 에러는 useAuth에서 처리됨 + console.error("로그인 실패:", error); + } + }; -export default function LoginPage() { return ( - -
+ +
+
+
+ + DaylyLog + +

로그인

+

+ 계정에 로그인하여 DaylyLog를 시작하세요. +

+
+ +
+
+
+ + + {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+
+ + {/* 전역 에러 표시 */} + {loginError && ( +

+ {loginError.message || + "로그인에 실패했습니다. 다시 시도해주세요."} +

+ )} + +
+ +
+ +
+ + 계정이 없으신가요? 회원가입 + + + 메인 화면으로 돌아가기 + +
+
- } - > - - +
+
); -} +}; + +export default LoginPage; diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index b8c4a4a..c3f48f9 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -316,21 +316,19 @@ export default function SignupPage() { -
+
- 계정이 없으신가요? 회원가입 + 회원이신가요? 로그인 + + + 메인 화면으로 돌아가기 -
- - 메인 화면으로 돌아가기 - -
diff --git a/src/components/auth/LoginPageContent.tsx b/src/components/auth/LoginPageContent.tsx deleted file mode 100644 index 988549f..0000000 --- a/src/components/auth/LoginPageContent.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import AuthGuard from "@/components/auth/AuthGuard"; -import { useAuth } from "@/hooks/auth"; -import { LoginFormValues, loginSchema } from "@/lib/validations/auth"; -import { zodResolver } from "@hookform/resolvers/zod"; -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useForm } from "react-hook-form"; - -export default function LoginPageContent() { - const router = useRouter(); - const searchParams = useSearchParams(); - const redirectUrl = searchParams.get("redirect") || "/dashboard"; - - const { login, isLoggingIn, loginError } = useAuth(); - - const { - register, - handleSubmit, - formState: { errors, isValid }, - } = useForm({ - resolver: zodResolver(loginSchema), - mode: "onChange", // 실시간 검증 - }); - - const onSubmit = async (data: LoginFormValues) => { - try { - await login(data); - router.push(redirectUrl); - } catch (error) { - // 에러는 useAuth에서 처리됨 - console.error("로그인 실패:", error); - } - }; - - return ( - -
-
-
- - DaylyLog - -

로그인

-

- 계정에 로그인하여 DaylyLog를 시작하세요 -

-
- -
- {/* 전역 에러 표시 */} - {loginError && ( -
-

- {loginError.message || - "로그인에 실패했습니다. 다시 시도해주세요."} -

-
- )} - -
-
- - - {errors.email && ( -

- {errors.email.message} -

- )} -
- -
- - - {errors.password && ( -

- {errors.password.message} -

- )} -
-
- -
- -
- -
- - 계정이 없으신가요? 회원가입 - -
-
-
-
-
- ); -} diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 15d5256..a4f5c4e 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -116,12 +116,94 @@ export const useAuth = () => { // 로그아웃 const logoutMutation = useMutation({ mutationFn: async () => { - const { error } = await supabase.auth.signOut(); - if (error) throw error; + try { + // 1단계: Supabase 로그아웃 + const { error } = await supabase.auth.signOut(); + if (error) { + console.warn("Supabase 로그아웃 오류:", error); + } + + // 2단계: access_token 강제 제거 + if (typeof window !== "undefined") { + // 현재 Supabase 프로젝트의 정확한 storage key 찾기 + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const projectRef = supabaseUrl.split("//")[1].split(".")[0]; + const storageKey = `sb-${projectRef}-auth-token`; + + // access_token이 포함된 키들 정리 + const keysToRemove = [ + storageKey, + "supabase.auth.token", + "sb-auth-token", + `sb-${projectRef}-auth-token`, + "daylylog-auth-token", + ]; + + keysToRemove.forEach((key) => { + try { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + } catch (e) { + console.warn(`토큰 키 제거 실패: ${key}`, e); + } + }); + + // localStorage에서 access_token이 포함된 모든 키 검색 및 제거 + const allKeys = Object.keys(localStorage); + allKeys.forEach((key) => { + try { + const value = localStorage.getItem(key); + if ( + value && + (value.includes("access_token") || + value.includes("refresh_token") || + key.includes("supabase") || + key.includes("sb-")) + ) { + localStorage.removeItem(key); + } + } catch (e) { + // 파싱 오류 무시 + } + }); + + // 세션 스토리지도 동일하게 처리 + const sessionKeys = Object.keys(sessionStorage); + sessionKeys.forEach((key) => { + try { + const value = sessionStorage.getItem(key); + if ( + value && + (value.includes("access_token") || + value.includes("refresh_token") || + key.includes("supabase") || + key.includes("sb-")) + ) { + sessionStorage.removeItem(key); + } + } catch (e) { + // 파싱 오류 무시 + } + }); + } + } catch (error) { + console.error("로그아웃 처리 중 오류:", error); + // 오류 발생 시에도 강제 정리 + if (typeof window !== "undefined") { + localStorage.clear(); + sessionStorage.clear(); + } + } }, onSuccess: () => { queryClient.clear(); }, + onSettled: () => { + // 성공/실패 관계없이 페이지 새로고침으로 완전 초기화 + if (typeof window !== "undefined") { + window.location.replace("/"); + } + }, }); // 프로필 업데이트 @@ -201,7 +283,7 @@ export const useAuth = () => { user: session?.user || null, profile, session, - isLoading: (isSessionLoading || isProfileLoading) && !isInitialized, + isLoading: isSessionLoading || (!isInitialized && session === undefined), isAuthenticated: !!session?.user, emailCheckResult: checkEmailMutation.data, nicknameCheckResult: checkNicknameMutation.data,