diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f9208ce --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md (auto-trade) + +## 기본 원칙 +- 모든 응답과 설명은 한국어로 작성. +- 쉬운 말로 설명. 어려운 용어는 괄호로 짧게 뜻을 덧붙임. +- 요청이 모호하면 먼저 질문 1~3개로 범위를 확인. + +## 프로젝트 요약 +- Next.js 16 App Router, React 19, TypeScript +- 상태 관리: zustand +- 데이터: Supabase +- 폼 및 검증: react-hook-form, zod +- UI: Tailwind CSS v4, Radix UI (components.json 사용) + +## 명령어 +- 개발 서버: (포트는 3001번이야) +pm run dev +- 린트: +pm run lint +- 빌드: +pm run build +- 실행: +pm run start + +## 코드 및 문서 규칙 +- JSX 섹션 주석 형식: {/* ========== SECTION NAME ========== */} +- 함수 및 컴포넌트 JSDoc에 @see 필수 (호출 파일, 함수 또는 이벤트 이름, 목적 포함) +- 상태 정의, 이벤트 핸들러, 복잡한 JSX 로직에는 인라인 주석을 충분히 작성 + +## 설명 방식 +- 단계별로 짧게, 예시는 1개만. +- 사용자가 요청한 변경과 이유를 함께 설명. +- 파일 경로는 pp/...처럼 코드 형식으로 표기. + +## 여러 도구를 함께 쓸 때 (쉬운 설명) +- 기준 설명을 한 군데에 모아두고, 그 파일만 계속 업데이트하는 것이 핵심. +- 예를 들어 PROJECT_CONTEXT.md에 스택, 폴더 구조, 규칙을 적어둔다. +- 그리고 각 도구의 규칙 파일에 PROJECT_CONTEXT.md를 참고라고 써 둔다. +- 이렇게 하면 어떤 도구를 쓰든 같은 파일을 읽게 되어, 매번 다시 설명할 일이 줄어든다. diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md new file mode 100644 index 0000000..a4cee4e --- /dev/null +++ b/PROJECT_CONTEXT.md @@ -0,0 +1,48 @@ +# PROJECT_CONTEXT.md + +이 파일은 프로젝트 설명의 기준(원본)입니다. +여기만 업데이트하고, 다른 도구 규칙에서는 이 파일을 참고하도록 연결하세요. + +## 한 줄 요약 +- 자동매매(오토 트레이드) 웹 앱 + +## 기술 스택 +- Next.js 16 (App Router) +- React 19, TypeScript +- 상태 관리: zustand +- 데이터: Supabase +- 폼/검증: react-hook-form, zod +- UI: Tailwind CSS v4, Radix UI + +## 폴더 구조(핵심만) +- pp/ 라우팅 및 페이지 +- eatures/ 도메인별 기능 +- components/ 공용 UI +- lib/ 유틸/클라이언트 +- utils/ 헬퍼 + +## 주요 규칙(요약) +- JSX 섹션 주석 형식: {/* ========== SECTION NAME ========== */} +- 함수/컴포넌트 JSDoc에 @see 필수 +- 파일 상단에 @author jihoon87.lee +- 상태/이벤트/복잡 로직에 인라인 주석 충분히 작성 + +## 작업 흐름 +- 개발 서버: +pm run dev +- 린트: +pm run lint +- 빌드: +pm run build +- 실행: +pm run start + +## 자주 하는 설명 템플릿 +- 변경 이유: (왜 바꾸는지) +- 변경 내용: (무엇을 바꾸는지) +- 영향 범위: (어디에 영향이 있는지) + +## 업데이트 가이드 +- 새 규칙/패턴이 생기면 여기에 먼저 추가 +- 문장이 길어지면 더 짧게 요약 +- 도구별 규칙 파일에는 PROJECT_CONTEXT.md 참고만 적기 diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index 604d5f9..6fe38d4 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -1,20 +1,33 @@ import { createClient } from "@/utils/supabase/server"; -import { NextResponse } from "next/server"; +import { type NextRequest, NextResponse } from "next/server"; // NextRequest 추가 import { AUTH_ERROR_MESSAGES, AUTH_ROUTES } from "@/features/auth/constants"; import { getAuthErrorMessage } from "@/features/auth/errors"; /** - * OAuth/??? ?? ?? ?? + * OAuth/이메일 인증 콜백 처리 + * + * Supabase 인증 후 리다이렉트되는 라우트입니다. + * - 인증 코드를 세션으로 교환합니다. + * - 인증 에러를 처리합니다. + * - 최종 목적지(Next URL)로 리다이렉트합니다. */ -export async function GET(request: Request) { - const { searchParams, origin } = new URL(request.url); +export async function GET(request: NextRequest) { + // -------------------------------------------------------------------------- + // 1. 요청 파라미터 및 URL 준비 + // -------------------------------------------------------------------------- + const requestUrl = request.nextUrl.clone(); // URL 조작을 위해 복제 + const code = requestUrl.searchParams.get("code"); + const next = requestUrl.searchParams.get("next") ?? AUTH_ROUTES.HOME; - const code = searchParams.get("code"); - const next = searchParams.get("next") ?? AUTH_ROUTES.HOME; - const error = searchParams.get("error"); - const error_code = searchParams.get("error_code"); - const error_description = searchParams.get("error_description"); + // 에러 파라미터 확인 + const error = requestUrl.searchParams.get("error"); + const error_code = requestUrl.searchParams.get("error_code"); + const error_description = requestUrl.searchParams.get("error_description"); + const origin = requestUrl.origin; + // -------------------------------------------------------------------------- + // 2. 초기 에러 처리 (Provider 레벨 에러) + // -------------------------------------------------------------------------- if (error) { console.error("Auth callback error parameter:", { error, @@ -30,31 +43,46 @@ export async function GET(request: Request) { message = AUTH_ERROR_MESSAGES.OAUTH_SERVER_ERROR; } + // 로그인 페이지로 에러와 함께 이동 return NextResponse.redirect( `${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(message)}`, ); } + // -------------------------------------------------------------------------- + // 3. 인증 코드 교환 (Supabase 공식 패턴 적용) + // -------------------------------------------------------------------------- if (code) { const supabase = await createClient(); + + // 코드 교환 실행 const { error: exchangeError } = await supabase.auth.exchangeCodeForSession(code); if (!exchangeError) { - const forwardedHost = request.headers.get("x-forwarded-host"); + // ---------------------------------------------------------------------- + // 3-1. 교환 성공: 리다이렉트 처리 + // 코드 파라미터 등을 제거하고 깨끗한 URL로 이동합니다. + // ---------------------------------------------------------------------- + const forwardedHost = request.headers.get("x-forwarded-host"); // 로드밸런서 지원 const isLocalEnv = process.env.NODE_ENV === "development"; + // 리다이렉트할 최종 URL 설정 if (isLocalEnv) { + // 로컬 개발 환경 + return NextResponse.redirect(`${origin}${next}`); + } else if (forwardedHost) { + // 프로덕션 환경 (Vercel 등 프록시 뒤) + return NextResponse.redirect(`https://${forwardedHost}${next}`); + } else { + // 기본 return NextResponse.redirect(`${origin}${next}`); } - - if (forwardedHost) { - return NextResponse.redirect(`https://${forwardedHost}${next}`); - } - - return NextResponse.redirect(`${origin}${next}`); } + // ------------------------------------------------------------------------ + // 3-2. 교환 실패: 에러 처리 + // ------------------------------------------------------------------------ console.error("Auth exchange error:", exchangeError.message); const message = getAuthErrorMessage(exchangeError); return NextResponse.redirect( @@ -62,6 +90,9 @@ export async function GET(request: Request) { ); } + // -------------------------------------------------------------------------- + // 4. 잘못된 접근 처리 + // -------------------------------------------------------------------------- const errorMessage = encodeURIComponent( AUTH_ERROR_MESSAGES.INVALID_AUTH_LINK, );