feat(WEB): 작업지시 수정 페이지 및 생산 관리 기능 개선

신규 기능:
- 작업지시 수정 페이지 추가 (/production/work-orders/[id]/edit)
- WorkOrderEdit 컴포넌트 신규 생성
- bulk-actions.ts 일괄 작업 유틸리티 추가
- toast-utils.ts 알림 유틸리티 추가

기능 개선:
- ProductionDashboard 대시보드 액션 및 표시 개선
- WorkOrderCreate 생성 화면 개선
- WorkResultList 작업 결과 목록 타입 및 표시 개선
- EstimateDetailForm 견적 폼 개선
- QuoteRegistration 견적 등록 개선
- client-management-sales-admin 거래처 관리 개선
- error-handler.ts 에러 처리 개선
This commit is contained in:
2026-01-16 15:39:02 +09:00
parent 34deb61632
commit 98b65a6ca4
18 changed files with 1157 additions and 89 deletions

View File

@@ -178,6 +178,9 @@ export function QuoteRegistration({
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
const [isCalculating, setIsCalculating] = useState(false);
// 카테고리별 완제품 캐시 (API 재호출 최소화)
const [categoryProductsCache, setCategoryProductsCache] = useState<Record<string, FinishedGoods[]>>({});
// 거래처 목록 상태 (API에서 로드)
const [clients, setClients] = useState<Vendor[]>([]);
const [isLoadingClients, setIsLoadingClients] = useState(false);
@@ -203,14 +206,20 @@ export function QuoteRegistration({
}, 0);
}, [calculationResults, formData.items]);
// 컴포넌트 마운트 시 완제품 목록 로드
// 컴포넌트 마운트 시 완제품 목록 로드 (초기 로드는 size 제한 없이 - 카테고리별 호출로 대체됨)
useEffect(() => {
const loadFinishedGoods = async () => {
const loadInitialProducts = async () => {
setIsLoadingProducts(true);
try {
// 초기에는 ALL 카테고리로 로드 (size 제한 내에서)
const result = await getFinishedGoods();
if (result.success) {
setFinishedGoods(result.data);
// 캐시에도 저장
setCategoryProductsCache(prev => ({
...prev,
"ALL": result.data
}));
} else {
toast.error(`완제품 목록 로드 실패: ${result.error}`);
}
@@ -221,7 +230,7 @@ export function QuoteRegistration({
setIsLoadingProducts(false);
}
};
loadFinishedGoods();
loadInitialProducts();
}, []);
// 컴포넌트 마운트 시 거래처 목록 로드
@@ -280,12 +289,41 @@ export function QuoteRegistration({
}
}, [editingQuote]);
// 카테고리별 완제품 필터링
// 카테고리별 완제품 로드 (API 호출)
const loadProductsByCategory = async (category: string) => {
// 이미 캐시에 있으면 스킵
if (categoryProductsCache[category]) {
return;
}
setIsLoadingProducts(true);
try {
// 카테고리가 ALL이면 전체, 아니면 해당 카테고리만 조회
const result = await getFinishedGoods(category === "ALL" ? undefined : category);
if (result.success) {
setCategoryProductsCache(prev => ({
...prev,
[category]: result.data
}));
} else {
toast.error(`완제품 목록 로드 실패: ${result.error}`);
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
toast.error("완제품 목록을 불러오는데 실패했습니다.");
} finally {
setIsLoadingProducts(false);
}
};
// 카테고리별 완제품 조회 (캐시 기반)
const getFilteredProducts = (category: string) => {
if (!category || category === "ALL") {
return finishedGoods; // 전체 선택 시 모든 완제품
// 전체 선택 시 캐시된 ALL 데이터 또는 초기 finishedGoods
return categoryProductsCache["ALL"] || finishedGoods;
}
return finishedGoods.filter(fg => fg.item_category === category);
// 카테고리별 캐시 반환
return categoryProductsCache[category] || [];
};
// 유효성 검사
@@ -395,9 +433,11 @@ export function QuoteRegistration({
const newItems = [...formData.items];
newItems[index] = { ...newItems[index], [field]: value };
// 제품 카테고리 변경 시 제품명 초기화
if (field === "productCategory") {
// 제품 카테고리 변경 시 제품명 초기화 및 해당 카테고리 제품 로드
if (field === "productCategory" && typeof value === "string") {
newItems[index].productName = "";
// 해당 카테고리 제품 목록 API 호출 (캐시 없으면)
loadProductsByCategory(value);
}
setFormData({ ...formData, items: newItems });

View File

@@ -810,7 +810,7 @@ export async function getFinishedGoods(category?: string): Promise<{
if (category) {
searchParams.set('item_category', category);
}
searchParams.set('size', '1000'); // 전체 조회
searchParams.set('size', '5000'); // 전체 조회 (테넌트별 FG 품목 수에 따라 조정)
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?${searchParams.toString()}`;
@@ -863,8 +863,8 @@ export async function getFinishedGoods(category?: string): Promise<{
success: true,
data: items.map((item: Record<string, unknown>) => ({
id: item.id,
item_code: item.code as string, // Item 모델은 'code' 필드 사용
item_name: item.name as string, // Item 모델은 'name' 필드 사용
item_code: (item.item_code || item.code) as string, // API가 code → item_code로 변환
item_name: item.name as string,
item_category: (item.item_category as string) || '',
specification: item.specification as string | undefined,
unit: item.unit as string | undefined,