Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/apis/authApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { clientApiClient } from '@/lib/client/apiClient';
import { ApiError } from '@/lib/errors/ApiError';
import { User } from '@/types/user';

interface UserInfoResponse {
success: boolean;
data?: User;
message?: string;
}

export const fetchUserInfo = async (): Promise<User> => {
const response = await clientApiClient<UserInfoResponse>('/api/auth/me');

if (!response.success || !response.data) {
throw new ApiError(200, response.message || 'Failed to fetch user info', response);
}

return response.data;
};

export const logout = async (): Promise<void> => {
await clientApiClient('/api/auth/logout', {
method: 'POST',
});
};
79 changes: 20 additions & 59 deletions src/apis/chatApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type SafeFetchOptions, safeFetch } from '@/hooks/util/api/fetch/safeFetch';
// 백엔드 직접 호출
import { backendApiClient } from '@/lib/client/backendClient';
import type {
ChatListApiResponse,
ChatRoom,
Expand All @@ -7,48 +8,14 @@ import type {
DeleteChatApiResponse,
} from '@/types/api/chatApi';

const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;

if (!API_BASE_URL) {
throw new Error('Missing environment variable: NEXT_PUBLIC_BASE_API_URL');
}

if (!API_TOKEN) {
throw new Error('Missing environment variable: NEXT_PUBLIC_API_TOKEN');
}

const buildUrl = (...paths: string[]) => `${API_BASE_URL}${paths.join('')}`;

const authHeaderValue = () => `Bearer ${API_TOKEN}`;

const withAuth = (init?: SafeFetchOptions): SafeFetchOptions => {
const headers: HeadersInit = {
Authorization: authHeaderValue(),
...(init?.headers ?? {}),
};

return {
timeout: 15_000,
jsonContentTypeCheck: true,
...init,
headers,
};
};

const CHATS_ENDPOINT = buildUrl('/v1/chats');

export const fetchChats = async (): Promise<ChatRoom[]> => {
const body = await safeFetch<ChatListApiResponse>(
CHATS_ENDPOINT,
withAuth({ cache: 'no-store' })
);
const response = await backendApiClient<ChatListApiResponse>('/v1/chats');

if (!body?.data || !body.success) {
throw new Error(body?.message ?? 'Invalid response');
if (!response.success || !response.data) {
throw new Error(response.message ?? 'Failed to fetch chats');
}

const chats = body.data.chats;
const chats = response.data.chats;

if (!Array.isArray(chats)) {
throw new Error('Invalid response: chats is not an array');
Expand All @@ -58,32 +25,26 @@ export const fetchChats = async (): Promise<ChatRoom[]> => {
};

export const createChat = async (payload: CreateChatPayload): Promise<ChatRoom> => {
const body = await safeFetch<CreateChatApiResponse>(
CHATS_ENDPOINT,
withAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
);
const response = await backendApiClient<CreateChatApiResponse>('/v1/chats', {
method: 'POST',
body: JSON.stringify(payload),
});

if (!body?.data || !body.success) {
throw new Error(body?.message ?? 'Invalid response');
if (!response.success || !response.data) {
throw new Error(response.message ?? 'Failed to create chat');
}

return body.data;
return response.data;
};

export const deleteChat = async (id: number): Promise<DeleteChatApiResponse> => {
const body = await safeFetch<DeleteChatApiResponse>(
`${CHATS_ENDPOINT}/${id}`,
withAuth({
method: 'DELETE',
})
);
if (!body || typeof body.success !== 'boolean' || !body.status || !body.message) {
throw new Error(body?.message ?? 'Invalid response');
const response = await backendApiClient<DeleteChatApiResponse>(`/v1/chats/${id}`, {
method: 'DELETE',
});

if (!response.success) {
throw new Error(response.message ?? 'Failed to delete chat');
}

return body;
return response;
};
38 changes: 10 additions & 28 deletions src/apis/report.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,15 @@
import { SafeFetchOptions, safeFetch } from '@/hooks/util/api/fetch/safeFetch';
import { backendApiClient } from '@/lib/client/backendClient';
import type { ReportApiResponse, ReportRequest } from '@/types/api/report';

const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;

const authHeaderValue = () => `Bearer ${API_TOKEN}`;
export const createReport = async (payload: ReportRequest) => {
const res = await backendApiClient<ReportApiResponse>('/v1/report', {
method: 'POST',
body: JSON.stringify(payload),
});

const withAuth = (init?: SafeFetchOptions): SafeFetchOptions => {
const headers: HeadersInit = {
Authorization: authHeaderValue(),
...(init?.headers ?? {}),
};
if (!res.success || !res.data) {
throw new Error(res.message ?? 'Failed to create report');
}

return {
timeout: 15_000,
jsonContentTypeCheck: true,
...init,
headers,
};
};

export const createReport = async (payload: ReportRequest) => {
return safeFetch<ReportApiResponse>(
`${API_BASE_URL}/v1/report`,
withAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
cache: 'no-store',
})
);
return res.data;
};
23 changes: 0 additions & 23 deletions src/apis/userApi.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/(route)/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function Home() {
<div className="mb-7 ml-4 flex w-full max-w-184 flex-col">
<div className="font-title-md ml-6 flex flex-col gap-1.5 md:flex-row">
<span>저장한 링크 속 내용을 바탕으로</span>
<span> 답변해 드려요.</span>
<span> 답변해 드려요...</span>
</div>
<GreetingBlock context="default" />
</div>
Expand Down
33 changes: 14 additions & 19 deletions src/app/(route)/home/_components/useCreateChatRoom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createChat } from '@/apis/chatApi';
import { FetchError, ParseError, TimeoutError } from '@/hooks/util/api/error/errors';
import { BackendApiError } from '@/lib/client/backendClient';
import type { ChatRoom } from '@/types/api/chatApi';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
Expand Down Expand Up @@ -41,24 +41,19 @@ export function useCreateChatRoom() {
resetForm();
return created;
} catch (err) {
switch (true) {
case err instanceof FetchError && err.status === 401:
setError('로그인이 필요합니다.');
break;
case err instanceof FetchError && err.status === 400:
setError('요청 형식이 올바르지 않습니다.');
break;
case err instanceof TimeoutError:
setError('요청이 시간 초과되었습니다. 다시 시도해주세요.');
break;
case err instanceof ParseError:
setError('서버 응답을 처리하는 중 오류가 발생했습니다.');
break;
case err instanceof FetchError:
setError(err.message || '서버와 통신하는 중 오류가 발생했습니다.');
break;
default:
setError('알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
if (err instanceof BackendApiError) {
switch (err.status) {
case 401:
setError('로그인이 필요합니다.');
break;
case 400:
setError('요청 형식이 올바르지 않습니다.');
break;
default:
setError(err.message || '서버와 통신하는 중 오류가 발생했습니다.');
}
} else {
setError('알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
}
} finally {
setCreating(false);
Expand Down
12 changes: 11 additions & 1 deletion src/app/(route)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// app/home/page.tsx
import { cookies } from 'next/headers';

import Home from './HomePage';

export default function page() {
export const dynamic = 'force-dynamic';

export default async function page() {
const cookieStore = await cookies();
const token = cookieStore.get('accessToken');

console.log('🔥 Server side - Token exists:', !!token);

return (
<main>
<Home />
Expand Down
55 changes: 12 additions & 43 deletions src/app/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
import ICLandingBackground from '@/components/Icons/svgs/ic_landing_background.svg';
import ICLandingIcLogo from '@/components/Icons/svgs/ic_landing_ic_logo.svg';
import ICLandingTextLogo from '@/components/Icons/svgs/ic_landing_text_logo.svg';
import { setCookieUtil } from '@/hooks/useCookie';
import { COOKIES_KEYS } from '@/lib/constants/cookies';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

const ERROR_MESSAGES: Record<string, string> = {
auth_failed: '로그인에 실패했습니다.',
server_error: '서버 오류가 발생했습니다.',
unauthorized: '로그인이 필요합니다.',
session_expired: '세션이 만료되었습니다. 다시 로그인해주세요.',
};

export default function Landing() {
const searchParams = useSearchParams();
const router = useRouter();
const error = searchParams.get('error');
const router = useRouter();

const isDev = process.env.NODE_ENV === 'development';

// 개발용 가짜 로그인 TODO: 나중에 지우기(사실 랜딩을 다 갈아야하긴 하지만)
Expand All @@ -23,49 +28,21 @@ export default function Landing() {
console.error('NEXT_PUBLIC_API_TOKEN is missing');
return;
}

// 백엔드 인증 토큰 저장
setCookieUtil(COOKIES_KEYS.ACCESS_TOKEN, token, {
maxAge: 60 * 60 * 24, // 1일
path: '/',
});

// 개발용 유저 정보 (UI용)
setCookieUtil(
COOKIES_KEYS.USER_INFO,
JSON.stringify({
id: 'dev',
email: 'dev@test.com',
name: '개발자',
picture: '',
}),
{
maxAge: 60 * 60 * 24,
path: '/',
}
);

router.push('/home');
};

const handleGoogleLogin = () => {
window.location.href = `${process.env.NEXT_PUBLIC_BASE_API_URL}/oauth2/authorization/google`; // 백엔드 URL로 변경
window.location.href = '/api/auth/login'; // 백엔드 URL로 변경
};

useEffect(() => {
if (error) {
console.error('Login error:', error);
}
}, [error]);

return (
<div className="relative flex min-h-screen items-center justify-center bg-gray-50">
<div className="relative z-10 flex w-full max-w-md flex-col items-center px-6 text-center">
<div className="flex items-center gap-5">
<div className="h-[53px] w-[60px] [&>svg]:h-full [&>svg]:w-full" aria-hidden="true">
<ICLandingIcLogo />
</div>
<div className="h-[50px] w-[240px] [&>svg]:h-full [&>svg]:w-full" aria-label="Linkiving">
<div className="h-[50px] w-60 [&>svg]:h-full [&>svg]:w-full" aria-label="Linkiving">
<ICLandingTextLogo />
</div>
</div>
Expand All @@ -77,9 +54,7 @@ export default function Landing() {

{error && (
<div className="bg-red100 text-red700 mb-6 w-full rounded-lg p-4 text-sm">
{error === 'auth_failed' && '로그인에 실패했습니다.'}
{error === 'server_error' && '서버 오류가 발생했습니다.'}
{!['auth_failed', 'server_error'].includes(error) && '오류가 발생했습니다.'}
{ERROR_MESSAGES[error] ?? '오류가 발생했습니다.'}
<br />
잠시 후 다시 시도해주세요.
</div>
Expand All @@ -94,12 +69,6 @@ export default function Landing() {
🔧 개발 모드 로그인
</button>
)}
<button
onClick={() => router.push('/signup')}
className="mb-3 flex w-full items-center justify-center rounded-full border border-gray-300 bg-white px-6 py-4 font-medium text-gray-700 shadow-sm transition hover:border-gray-400 hover:bg-gray-50"
>
회원가입 하기
</button>

<button
onClick={handleGoogleLogin}
Expand Down
5 changes: 5 additions & 0 deletions src/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.redirect('/home');
}
7 changes: 7 additions & 0 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.redirect(
`${process.env.NEXT_PUBLIC_BASE_API_URL}/oauth2/authorization/google`
);
}
Loading