feat: 생산/품질/자재/출고/주문 관리 페이지 구현
- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,14 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { toast } from 'sonner';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
effectiveDate: "적용일",
|
||||
purchasePrice: "입고가",
|
||||
salesPrice: "판매단가",
|
||||
};
|
||||
|
||||
import type {
|
||||
PricingData,
|
||||
@@ -105,7 +113,7 @@ export function PricingFormClient({
|
||||
const [unit, setUnit] = useState(displayUnit);
|
||||
|
||||
// 에러 상태
|
||||
const [errors, setErrors] = useState<Record<string, boolean>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [showHistoryDialog, setShowHistoryDialog] = useState(false);
|
||||
@@ -187,11 +195,11 @@ export function PricingFormClient({
|
||||
|
||||
// 유효성 검사
|
||||
const validateForm = useCallback(() => {
|
||||
const newErrors: Record<string, boolean> = {};
|
||||
if (!effectiveDate) newErrors.effectiveDate = true;
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!effectiveDate) newErrors.effectiveDate = "적용일을 선택해주세요";
|
||||
if (purchasePrice <= 0 && salesPrice <= 0) {
|
||||
newErrors.purchasePrice = true;
|
||||
newErrors.salesPrice = true;
|
||||
newErrors.purchasePrice = "입고가 또는 판매단가 중 최소 하나를 입력해주세요";
|
||||
newErrors.salesPrice = "입고가 또는 판매단가 중 최소 하나를 입력해주세요";
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
@@ -200,10 +208,14 @@ export function PricingFormClient({
|
||||
// 저장 처리
|
||||
const handleSave = async (isRevision = false, revisionReason = '') => {
|
||||
if (!validateForm()) {
|
||||
toast.error('필수 항목을 입력해주세요.');
|
||||
// 페이지 상단으로 스크롤
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 에러 초기화
|
||||
setErrors({});
|
||||
|
||||
// 수정 모드이고 리비전 있으면 수정 이력 다이얼로그
|
||||
if (isEditMode && initialData && !isRevision &&
|
||||
(initialData.currentRevision > 0 || initialData.isFinal)) {
|
||||
@@ -301,6 +313,35 @@ export function PricingFormClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200 mb-6">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({Object.keys(errors).length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{Object.entries(errors).map(([field, message]) => {
|
||||
const fieldName = FIELD_NAME_MAP[field] || field;
|
||||
return (
|
||||
<li key={field} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>
|
||||
<strong>{fieldName}</strong>: {message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 상태 표시 (수정 모드) */}
|
||||
{isEditMode && initialData && (
|
||||
<div className="mb-4 flex gap-2 justify-end">
|
||||
@@ -383,7 +424,7 @@ export function PricingFormClient({
|
||||
value={effectiveDate}
|
||||
onChange={(e) => {
|
||||
setEffectiveDate(e.target.value);
|
||||
setErrors((prev) => ({ ...prev, effectiveDate: false }));
|
||||
setErrors((prev) => { const n = {...prev}; delete n.effectiveDate; return n; });
|
||||
}}
|
||||
className={errors.effectiveDate ? 'border-red-500' : ''}
|
||||
/>
|
||||
@@ -439,7 +480,7 @@ export function PricingFormClient({
|
||||
value={purchasePrice || ''}
|
||||
onChange={(e) => {
|
||||
setPurchasePrice(parseInt(e.target.value) || 0);
|
||||
setErrors((prev) => ({ ...prev, purchasePrice: false, salesPrice: false }));
|
||||
setErrors((prev) => { const n = {...prev}; delete n.purchasePrice; delete n.salesPrice; return n; });
|
||||
}}
|
||||
placeholder="0"
|
||||
className={errors.purchasePrice ? 'border-red-500 pr-12' : 'pr-12'}
|
||||
@@ -619,7 +660,7 @@ export function PricingFormClient({
|
||||
value={salesPrice || ''}
|
||||
onChange={(e) => {
|
||||
handleSalesPriceChange(parseInt(e.target.value) || 0);
|
||||
setErrors((prev) => ({ ...prev, purchasePrice: false, salesPrice: false }));
|
||||
setErrors((prev) => { const n = {...prev}; delete n.purchasePrice; delete n.salesPrice; return n; });
|
||||
}}
|
||||
placeholder="0"
|
||||
className={errors.salesPrice ? 'border-red-500 pr-12' : 'pr-12'}
|
||||
|
||||
Reference in New Issue
Block a user