feat: 견적확정 밸리데이션, 수주등록 개소그룹, 작업지시 개선

- 견적확정 시 업체명/현장명/담당자/연락처 프론트 밸리데이션 추가
- 견적확정 후 수주등록 버튼 동적 전환
- 수주등록 품목 개소별(floor+code) 그룹핑 수정
- 작업지시 상세 quantity 문자열→숫자 변환 (formatQuantity)
- 작업지시 탭 카운트 초기 로딩 시 전체 표시 (by_process 활용)
- 작업지시 상세 개소별/품목별 합산 테이블 추가
- 작업자 화면 API 연동 및 목업 데이터 분리
- 입고관리 완료건 수정, 재고현황 개선
This commit is contained in:
2026-02-07 03:27:23 +09:00
parent b2085a84ca
commit a8591c438e
29 changed files with 3238 additions and 700 deletions

View File

@@ -70,13 +70,10 @@ export function ItemSearchModal({
}
}, [itemType]);
// 검색어 유효성 검사: 영문 1자 이상 또는 한글 1자 이상
// 검색어 유효성 검사: 영문, 한글, 숫자 1자 이상
const isValidSearchQuery = useCallback((query: string) => {
if (!query) return false;
// 영문 1자 이상 또는 한글 1자 이상
const hasEnglish = /[a-zA-Z]/.test(query);
const hasKorean = /[가-힣ㄱ-ㅎㅏ-ㅣ]/.test(query);
return hasEnglish || hasKorean;
if (!query || !query.trim()) return false;
return /[a-zA-Z가-힣ㄱ-ㅎㅏ-ㅣ0-9]/.test(query);
}, []);
// 모달 열릴 때 초기화 (자동 로드 안함)
@@ -167,7 +164,7 @@ export function ItemSearchModal({
{!searchQuery
? "품목코드 또는 품목명을 입력하세요"
: !isValidSearchQuery(searchQuery)
? "영문 또는 한글 1자 이상 입력하세요"
? "영문, 한글 또는 숫자 1자 이상 입력하세요"
: "검색 결과가 없습니다"}
</div>
) : (

View File

@@ -188,8 +188,8 @@ export function QuoteFooterBar({
</Button>
)}
{/* 견적완료 - edit 모드에서만 표시 (final 상태가 아닐 때만) */}
{!isViewMode && status !== "final" && (
{/* 견적확정 - final 상태가 아닐 때 표시 (view/edit 모두) */}
{status !== "final" && (
<Button
onClick={onFinalize}
disabled={isSaving || totalAmount === 0}
@@ -201,7 +201,7 @@ export function QuoteFooterBar({
) : (
<Check className="h-4 w-4 md:mr-2" />
)}
<span className="hidden md:inline"></span>
<span className="hidden md:inline">{isViewMode ? "견적확정" : "견적완료"}</span>
</Button>
)}

View File

@@ -56,7 +56,8 @@ import { quoteRegistrationCreateConfig, quoteRegistrationEditConfig } from "./qu
import { FormSection } from "@/components/organisms/FormSection";
import { FormFieldGrid } from "@/components/organisms/FormFieldGrid";
import { FormField } from "../molecules/FormField";
import { getFinishedGoods, calculateBomBulk, getSiteNames, type FinishedGoods, type BomCalculationResult } from "./actions";
import { getFinishedGoods, calculateBomBulk, getSiteNames, type FinishedGoods } from "./actions";
import type { BomCalculationResult } from "./types";
import { getClients } from "../accounting/VendorManagement/actions";
import { isNextRedirectError } from "@/lib/utils/redirect-error";
import type { Vendor } from "../accounting/VendorManagement";

View File

@@ -693,6 +693,19 @@ export function QuoteRegistrationV2({
const handleSave = useCallback(async (saveType: "temporary" | "final") => {
if (!onSave) return;
// 확정 시 필수 필드 밸리데이션
if (saveType === "final") {
const missing: string[] = [];
if (!formData.clientName?.trim()) missing.push("업체명");
if (!formData.siteName?.trim()) missing.push("현장명");
if (!formData.managerName?.trim()) missing.push("담당자");
if (!formData.managerContact?.trim()) missing.push("연락처");
if (missing.length > 0) {
toast.error(`견적확정을 위해 다음 항목을 입력해주세요: ${missing.join(", ")}`);
return;
}
}
setIsSaving(true);
try {
const dataToSave: QuoteFormDataV2 = {
@@ -700,7 +713,10 @@ export function QuoteRegistrationV2({
status: saveType === "temporary" ? "temporary" : "final",
};
await onSave(dataToSave, saveType);
toast.success(saveType === "temporary" ? "저장되었습니다." : "견적이 확정되었습니다.");
// 확정 성공 시 상태 즉시 반영 → 견적확정 버튼 → 수주등록 버튼으로 전환
if (saveType === "final") {
setFormData(prev => ({ ...prev, status: "final" }));
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
const message = error instanceof Error ? error.message : "저장 중 오류가 발생했습니다.";

View File

@@ -902,8 +902,8 @@ export interface BomCalculateItem {
inspectionFee?: number;
}
// BomCalculationResult는 types.ts에서 import하고 re-export
export type { BomCalculationResult } from './types';
// BomCalculationResult는 types.ts에서 직접 import하세요
// import type { BomCalculationResult } from './types';
// API 서버 응답 구조 (QuoteCalculationService::calculateBomBulk)
export interface BomBulkResponse {