Fix: 인증 콜백 처리 개선 및 프로젝트 문서 추가
app/auth/callback/route.ts - NextRequest 타입 사용으로 요청/URL 파라미터 처리 개선 - 에러 파라미터 초기 처리 추가 및 사용자 메시지 매핑 - Supabase 코드 교환 흐름 정리(성공/실패 처리 분리), 로컬/프록시 환경에 따른 리다이렉트 로직 보강 - 잘못된 접근(인증 링크 오류) 처리 추가 및 로깅 개선 AGENTS.md - 개발 규칙, 명령어, 설명 방식 등 에이전트용 가이드 문서 추가 (한국어 규칙 포함) PROJECT_CONTEXT.md - 프로젝트 기술 스택, 폴더 구조, 주요 규칙 및 작업 흐름을 정리한 기준 문서 추가
This commit is contained in:
39
AGENTS.md
Normal file
39
AGENTS.md
Normal file
@@ -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를 참고라고 써 둔다.
|
||||||
|
- 이렇게 하면 어떤 도구를 쓰든 같은 파일을 읽게 되어, 매번 다시 설명할 일이 줄어든다.
|
||||||
48
PROJECT_CONTEXT.md
Normal file
48
PROJECT_CONTEXT.md
Normal file
@@ -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 참고만 적기
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
import { createClient } from "@/utils/supabase/server";
|
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 { AUTH_ERROR_MESSAGES, AUTH_ROUTES } from "@/features/auth/constants";
|
||||||
import { getAuthErrorMessage } from "@/features/auth/errors";
|
import { getAuthErrorMessage } from "@/features/auth/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth/??? ?? ?? ??
|
* OAuth/이메일 인증 콜백 처리
|
||||||
|
*
|
||||||
|
* Supabase 인증 후 리다이렉트되는 라우트입니다.
|
||||||
|
* - 인증 코드를 세션으로 교환합니다.
|
||||||
|
* - 인증 에러를 처리합니다.
|
||||||
|
* - 최종 목적지(Next URL)로 리다이렉트합니다.
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams, origin } = new URL(request.url);
|
// --------------------------------------------------------------------------
|
||||||
|
// 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 = requestUrl.searchParams.get("error");
|
||||||
const error = searchParams.get("error");
|
const error_code = requestUrl.searchParams.get("error_code");
|
||||||
const error_code = searchParams.get("error_code");
|
const error_description = requestUrl.searchParams.get("error_description");
|
||||||
const error_description = searchParams.get("error_description");
|
const origin = requestUrl.origin;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 2. 초기 에러 처리 (Provider 레벨 에러)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Auth callback error parameter:", {
|
console.error("Auth callback error parameter:", {
|
||||||
error,
|
error,
|
||||||
@@ -30,31 +43,46 @@ export async function GET(request: Request) {
|
|||||||
message = AUTH_ERROR_MESSAGES.OAUTH_SERVER_ERROR;
|
message = AUTH_ERROR_MESSAGES.OAUTH_SERVER_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 로그인 페이지로 에러와 함께 이동
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
`${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(message)}`,
|
`${origin}${AUTH_ROUTES.LOGIN}?message=${encodeURIComponent(message)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 3. 인증 코드 교환 (Supabase 공식 패턴 적용)
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
if (code) {
|
if (code) {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
// 코드 교환 실행
|
||||||
const { error: exchangeError } =
|
const { error: exchangeError } =
|
||||||
await supabase.auth.exchangeCodeForSession(code);
|
await supabase.auth.exchangeCodeForSession(code);
|
||||||
|
|
||||||
if (!exchangeError) {
|
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";
|
const isLocalEnv = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
// 리다이렉트할 최종 URL 설정
|
||||||
if (isLocalEnv) {
|
if (isLocalEnv) {
|
||||||
|
// 로컬 개발 환경
|
||||||
return NextResponse.redirect(`${origin}${next}`);
|
return NextResponse.redirect(`${origin}${next}`);
|
||||||
}
|
} else if (forwardedHost) {
|
||||||
|
// 프로덕션 환경 (Vercel 등 프록시 뒤)
|
||||||
if (forwardedHost) {
|
|
||||||
return NextResponse.redirect(`https://${forwardedHost}${next}`);
|
return NextResponse.redirect(`https://${forwardedHost}${next}`);
|
||||||
}
|
} else {
|
||||||
|
// 기본
|
||||||
return NextResponse.redirect(`${origin}${next}`);
|
return NextResponse.redirect(`${origin}${next}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// 3-2. 교환 실패: 에러 처리
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
console.error("Auth exchange error:", exchangeError.message);
|
console.error("Auth exchange error:", exchangeError.message);
|
||||||
const message = getAuthErrorMessage(exchangeError);
|
const message = getAuthErrorMessage(exchangeError);
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
@@ -62,6 +90,9 @@ export async function GET(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// 4. 잘못된 접근 처리
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
const errorMessage = encodeURIComponent(
|
const errorMessage = encodeURIComponent(
|
||||||
AUTH_ERROR_MESSAGES.INVALID_AUTH_LINK,
|
AUTH_ERROR_MESSAGES.INVALID_AUTH_LINK,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user