Merge branch 'master' into master_api_test개발

# Conflicts:
#	src/app/[locale]/(protected)/sales/pricing-management/page.tsx
This commit is contained in:
2025-12-23 16:36:21 +09:00
50 changed files with 664 additions and 244 deletions

View File

@@ -3,6 +3,7 @@
import { useRouter, useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Board, BoardFormData } from '@/components/board/BoardManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -35,14 +36,7 @@ export default function BoardEditPage() {
};
if (!board) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
}
return (

View File

@@ -13,6 +13,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Board } from '@/components/board/BoardManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -54,14 +55,7 @@ export default function BoardDetailPage() {
};
if (!board) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
}
return (

View File

@@ -10,6 +10,7 @@
import { Suspense } from 'react';
import { AttendanceManagement } from '@/components/hr/AttendanceManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -22,17 +23,8 @@ export const metadata: Metadata = {
export default function AttendanceManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<AttendanceManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="근태 정보를 불러오는 중..." />}>
<AttendanceManagement />
</Suspense>
);
}

View File

@@ -3,6 +3,7 @@
import { useRouter, useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import { CardForm } from '@/components/hr/CardManagement/CardForm';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Card, CardFormData } from '@/components/hr/CardManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -45,14 +46,7 @@ export default function CardEditPage() {
};
if (!card) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="카드 정보를 불러오는 중..." />;
}
return (

View File

@@ -13,6 +13,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Card } from '@/components/hr/CardManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -64,14 +65,7 @@ export default function CardDetailPage() {
};
if (!card) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="카드 정보를 불러오는 중..." />;
}
return (

View File

@@ -7,6 +7,7 @@
import { Suspense } from 'react';
import { DepartmentManagement } from '@/components/hr/DepartmentManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -19,17 +20,8 @@ export const metadata: Metadata = {
export default function DepartmentManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<DepartmentManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="부서 정보를 불러오는 중..." />}>
<DepartmentManagement />
</Suspense>
);
}

View File

@@ -3,6 +3,7 @@
import { useRouter, useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Employee, EmployeeFormData } from '@/components/hr/EmployeeManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -58,14 +59,7 @@ export default function EmployeeEditPage() {
};
if (!employee) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="사원 정보를 불러오는 중..." />;
}
return <EmployeeForm mode="edit" employee={employee} onSave={handleSave} />;

View File

@@ -13,6 +13,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Employee } from '@/components/hr/EmployeeManagement/types';
// TODO: 실제 API에서 데이터 가져오기
@@ -77,14 +78,7 @@ export default function EmployeeDetailPage() {
};
if (!employee) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="사원 정보를 불러오는 중..." />;
}
return (

View File

@@ -10,6 +10,7 @@
import { Suspense } from 'react';
import { EmployeeManagement } from '@/components/hr/EmployeeManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -22,17 +23,8 @@ export const metadata: Metadata = {
export default function EmployeeManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<EmployeeManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="사원 정보를 불러오는 중..." />}>
<EmployeeManagement />
</Suspense>
);
}

View File

@@ -10,6 +10,7 @@
import { Suspense } from 'react';
import { SalaryManagement } from '@/components/hr/SalaryManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -22,17 +23,8 @@ export const metadata: Metadata = {
export default function SalaryManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<SalaryManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="급여 정보를 불러오는 중..." />}>
<SalaryManagement />
</Suspense>
);
}

View File

@@ -10,6 +10,7 @@
import { Suspense } from 'react';
import { VacationManagement } from '@/components/hr/VacationManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -22,17 +23,8 @@ export const metadata: Metadata = {
export default function VacationManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<VacationManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="휴가 정보를 불러오는 중..." />}>
<VacationManagement />
</Suspense>
);
}

View File

@@ -302,7 +302,7 @@ export default function EditItemPage() {
// - PT(부품): DynamicItemForm에서 자동계산한 code 사용 (조립/절곡/구매 각각 다른 규칙)
// - Material(SM, RM, CS): material_code = 품목명-규격
// 2025-12-15: item_type은 Request Body에서 필수 (ItemUpdateRequest validation)
let submitData = { ...data, item_type: itemType };
let submitData: DynamicFormData = { ...data, item_type: itemType };
if (itemType === 'FG') {
// FG는 품목명이 품목코드가 되므로 name 값으로 code 설정

View File

@@ -7,13 +7,8 @@ import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
* - AuthenticatedLayout 내에서 표시됨 (사이드바, 헤더 유지)
* - React Suspense 자동 적용
* - 페이지 전환 시 즉각적인 피드백
* - 공통 레이아웃 스타일로 통일
* - 공통 레이아웃 스타일로 통일 (min-h-[calc(100vh-200px)])
*/
export default function ProtectedLoading() {
return (
<PageLoadingSpinner
text="페이지를 불러오는 중..."
minHeight="min-h-[calc(100vh-200px)]"
/>
);
return <PageLoadingSpinner />;
}

View File

@@ -7,6 +7,7 @@
import { Suspense } from 'react';
import { ItemMasterDataManagement } from '@/components/items/ItemMasterDataManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import type { Metadata } from 'next';
/**
@@ -19,17 +20,8 @@ export const metadata: Metadata = {
export default function ItemMasterDataManagementPage() {
return (
<div>
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
}>
<ItemMasterDataManagement />
</Suspense>
</div>
<Suspense fallback={<ContentLoadingSpinner text="품목기준정보를 불러오는 중..." />}>
<ItemMasterDataManagement />
</Suspense>
);
}

View File

@@ -13,7 +13,7 @@ import {
clientToFormData,
} from "@/hooks/useClientList";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
export default function ClientEditPage() {
const router = useRouter();
@@ -60,11 +60,7 @@ export default function ClientEditPage() {
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
return <ContentLoadingSpinner text="거래처 정보를 불러오는 중..." />;
}
if (!editingClient) {

View File

@@ -19,7 +19,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Loader2 } from "lucide-react";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
export default function ClientDetailPage() {
const router = useRouter();
@@ -80,11 +80,7 @@ export default function ClientDetailPage() {
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
return <ContentLoadingSpinner text="거래처 정보를 불러오는 중..." />;
}
if (!client) {

View File

@@ -8,6 +8,7 @@ import { useRouter, useParams } from "next/navigation";
import { useState, useEffect } from "react";
import { QuoteRegistration, QuoteFormData, INITIAL_QUOTE_FORM } from "@/components/quotes/QuoteRegistration";
import { toast } from "sonner";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
// 샘플 견적 데이터 (TODO: API에서 가져오기)
const SAMPLE_QUOTE: QuoteFormData = {
@@ -82,14 +83,7 @@ export default function QuoteEditPage() {
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto" />
<p className="mt-2 text-sm text-gray-500"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
}
return (

View File

@@ -39,6 +39,7 @@ import {
FileCheck,
ShoppingCart,
} from "lucide-react";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
// 샘플 견적 데이터 (TODO: API에서 가져오기)
const SAMPLE_QUOTE: QuoteFormData = {
@@ -140,16 +141,7 @@ export default function QuoteDetailPage() {
}, 0) || 0;
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto" />
<p className="mt-2 text-sm text-gray-500">
...
</p>
</div>
</div>
);
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
}
if (!quote) {

View File

@@ -13,6 +13,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { MOCK_POPUPS, type Popup } from '@/components/settings/PopupManagement/types';
export default function PopupDetailPage() {
@@ -43,14 +44,7 @@ export default function PopupDetailPage() {
};
if (!popup) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
return <ContentLoadingSpinner text="팝업 정보를 불러오는 중..." />;
}
return (

View File

@@ -60,7 +60,7 @@ export async function GET(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,

View File

@@ -109,7 +109,7 @@ export async function POST(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({ user_id, user_pwd }),
});

View File

@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
});
console.log('✅ Backend logout API called successfully');

View File

@@ -49,7 +49,7 @@ export async function POST(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,

View File

@@ -64,7 +64,7 @@ export async function POST(request: NextRequest) {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify(body),
});

View File

@@ -45,7 +45,7 @@ async function refreshAccessToken(refreshToken: string): Promise<{
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
@@ -88,7 +88,7 @@ async function executeBackendRequest(
// FormData인 경우 Content-Type을 생략해야 브라우저가 boundary를 자동 설정
const headers: Record<string, string> = {
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
'X-API-KEY': process.env.API_KEY || '',
'Authorization': token ? `Bearer ${token}` : '',
};