chore(WEB): 다수 컴포넌트 개선 및 CEO 대시보드 추가

- CEO 대시보드 컴포넌트 추가
- AuthenticatedLayout 개선
- 각 모듈 actions.ts 에러 핸들링 개선
- API fetch-wrapper, refresh-token 로직 개선
- ReceivablesStatus 컴포넌트 업데이트
- globals.css 스타일 업데이트
- 기타 다수 컴포넌트 수정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-08 17:15:42 +09:00
parent 387672b5b2
commit 29e7b41615
92 changed files with 7695 additions and 409 deletions

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { refreshAccessToken } from '@/lib/api/refresh-token';
/**
* 🔵 Next.js 내부 API - 인증 상태 확인 (PHP 백엔드 X)
@@ -49,72 +50,48 @@ export async function GET(request: NextRequest) {
// Only has refresh token - try to refresh
if (refreshToken && !accessToken) {
console.log('🔄 Access token missing, attempting refresh...');
console.log('🔍 Refresh token exists:', refreshToken.substring(0, 20) + '...');
console.log('🔍 Backend URL:', process.env.NEXT_PUBLIC_API_URL);
console.log('🔄 [auth/check] Access token missing, attempting refresh...');
// Attempt token refresh
try {
const refreshResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
// 공유 캐시를 사용하는 refreshAccessToken 함수 사용
const refreshResult = await refreshAccessToken(refreshToken, 'auth/check');
console.log('🔍 Refresh API response status:', refreshResponse.status);
if (refreshResult.success && refreshResult.accessToken) {
console.log('✅ [auth/check] Token refreshed successfully');
if (refreshResponse.ok) {
const data = await refreshResponse.json();
// Set new tokens with Safari-compatible configuration
const isProduction = process.env.NODE_ENV === 'production';
// Set new tokens with Safari-compatible configuration
// Safari compatibility: Secure only in production (HTTPS)
const isProduction = process.env.NODE_ENV === 'production';
const accessTokenCookie = [
`access_token=${refreshResult.accessToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
`Max-Age=${refreshResult.expiresIn || 7200}`,
].join('; ');
const accessTokenCookie = [
`access_token=${data.access_token}`,
'HttpOnly', // ✅ JavaScript cannot access
...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production (Safari fix)
'SameSite=Lax', // ✅ CSRF protection (Lax for better compatibility)
'Path=/',
`Max-Age=${data.expires_in || 7200}`,
].join('; ');
const refreshTokenCookie = [
`refresh_token=${refreshResult.refreshToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
'Max-Age=604800',
].join('; ');
const refreshTokenCookie = [
`refresh_token=${data.refresh_token}`,
'HttpOnly', // ✅ JavaScript cannot access
...(isProduction ? ['Secure'] : []), // ✅ HTTPS only in production (Safari fix)
'SameSite=Lax', // ✅ CSRF protection (Lax for better compatibility)
'Path=/',
'Max-Age=604800', // 7 days (longer for refresh token)
].join('; ');
const response = NextResponse.json(
{ authenticated: true, refreshed: true },
{ status: 200 }
);
console.log('✅ Token auto-refreshed in auth check');
response.headers.append('Set-Cookie', accessTokenCookie);
response.headers.append('Set-Cookie', refreshTokenCookie);
const response = NextResponse.json(
{ authenticated: true, refreshed: true },
{ status: 200 }
);
response.headers.append('Set-Cookie', accessTokenCookie);
response.headers.append('Set-Cookie', refreshTokenCookie);
return response;
} else {
const errorData = await refreshResponse.text();
console.error('❌ Refresh API failed:', refreshResponse.status, errorData);
}
} catch (error) {
console.error('❌ Token refresh failed in auth check:', error);
return response;
}
// Refresh failed - not authenticated
console.log('⚠️ Returning 401 due to refresh failure');
console.log('⚠️ [auth/check] Refresh failed, returning 401');
return NextResponse.json(
{ error: 'Token refresh failed' },
{ status: 401 }

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { refreshAccessToken } from '@/lib/api/refresh-token';
/**
* 🔵 Next.js 내부 API - 토큰 갱신 프록시 (PHP 백엔드로 전달)
@@ -43,24 +44,14 @@ export async function POST(request: NextRequest) {
);
}
// Call PHP backend refresh API
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
// 공유 캐시를 사용하는 refreshAccessToken 함수 사용
const refreshResult = await refreshAccessToken(refreshToken, 'api/auth/refresh');
if (!response.ok) {
const isProduction = process.env.NODE_ENV === 'production';
if (!refreshResult.success || !refreshResult.accessToken) {
// Refresh token is invalid or expired
console.warn('⚠️ Token refresh failed - user needs to re-login');
const isProduction = process.env.NODE_ENV === 'production';
console.warn('⚠️ [api/auth/refresh] Token refresh failed - user needs to re-login');
// Clear all tokens
const clearAccessToken = `access_token=; HttpOnly; ${isProduction ? 'Secure; ' : ''}SameSite=Lax; Path=/; Max-Age=0`;
@@ -79,30 +70,24 @@ export async function POST(request: NextRequest) {
return failResponse;
}
const data = await response.json();
// Prepare response
const responseData = {
message: 'Token refreshed successfully',
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: data.expires_at,
expires_in: refreshResult.expiresIn,
};
// Set new HttpOnly cookies
const isProduction = process.env.NODE_ENV === 'production';
const accessTokenCookie = [
`access_token=${data.access_token}`,
`access_token=${refreshResult.accessToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
`Max-Age=${data.expires_in || 7200}`,
`Max-Age=${refreshResult.expiresIn || 7200}`,
].join('; ');
const refreshTokenCookie = [
`refresh_token=${data.refresh_token}`,
`refresh_token=${refreshResult.refreshToken}`,
'HttpOnly',
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
@@ -116,10 +101,10 @@ export async function POST(request: NextRequest) {
...(isProduction ? ['Secure'] : []),
'SameSite=Lax',
'Path=/',
`Max-Age=${data.expires_in || 7200}`,
`Max-Age=${refreshResult.expiresIn || 7200}`,
].join('; ');
console.log('✅ Token refresh successful - New tokens stored');
console.log('✅ [api/auth/refresh] Token refresh successful');
const successResponse = NextResponse.json(responseData, { status: 200 });