fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가

## 품목관리 수정 버그 수정
- FG(제품) 수정 시 품목명 반영 안되는 문제 해결
  - productName → name 필드 매핑 추가
  - FG 품목코드 = 품목명 동기화 로직 추가
- Materials(SM, RM, CS) 수정페이지 진입 오류 해결
- UNIQUE 제약조건 위반 오류 해결

## Sales 페이지
- 거래처관리 (client-management-sales-admin) 페이지 구현
- 견적관리 (quote-management) 페이지 구현
- 관련 컴포넌트 및 훅 추가

## 기타
- 회원가입 페이지 차단 처리
- 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-04 20:52:42 +09:00
parent 42f80e2b16
commit 751e65f59b
52 changed files with 8869 additions and 1088 deletions

View File

@@ -0,0 +1,607 @@
/**
* 거래처 등록/수정 컴포넌트
*
* ResponsiveFormTemplate 적용
* - 데스크톱/태블릿/모바일 통합 폼 레이아웃
* - 섹션 기반 정보 입력
* - 유효성 검사 및 에러 표시
*/
"use client";
import { useState, useEffect } from "react";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { Checkbox } from "../ui/checkbox";
import { Label } from "../ui/label";
import {
Building2,
UserCircle,
Phone,
CreditCard,
FileText,
AlertTriangle,
Calculator,
} from "lucide-react";
import { toast } from "sonner";
import {
ResponsiveFormTemplate,
FormSection,
FormFieldGrid,
} from "../templates/ResponsiveFormTemplate";
import { FormField } from "../molecules/FormField";
import {
ClientFormData,
INITIAL_CLIENT_FORM,
ClientType,
BadDebtProgress,
} from "../../hooks/useClientList";
interface ClientRegistrationProps {
onBack: () => void;
onSave: (client: ClientFormData) => Promise<void>;
editingClient?: ClientFormData | null;
isLoading?: boolean;
}
export function ClientRegistration({
onBack,
onSave,
editingClient,
isLoading = false,
}: ClientRegistrationProps) {
const [formData, setFormData] = useState<ClientFormData>(
editingClient || INITIAL_CLIENT_FORM
);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSaving, setIsSaving] = useState(false);
// editingClient가 변경되면 formData 업데이트
useEffect(() => {
if (editingClient) {
setFormData(editingClient);
}
}, [editingClient]);
// 유효성 검사
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.name || formData.name.length < 2) {
newErrors.name = "거래처명은 2자 이상 입력해주세요";
}
if (!formData.businessNo || !/^\d{10}$/.test(formData.businessNo)) {
newErrors.businessNo = "사업자등록번호는 10자리 숫자여야 합니다";
}
if (!formData.representative || formData.representative.length < 2) {
newErrors.representative = "대표자명은 2자 이상 입력해주세요";
}
// 전화번호 형식 검사 (선택적)
const phonePattern = /^[0-9-]+$/;
if (formData.phone && !phonePattern.test(formData.phone)) {
newErrors.phone = "올바른 전화번호 형식이 아닙니다";
}
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = "올바른 이메일 형식이 아닙니다";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm()) {
toast.error("입력 내용을 확인해주세요.");
return;
}
setIsSaving(true);
try {
await onSave(formData);
toast.success(
editingClient ? "거래처가 수정되었습니다." : "거래처가 등록되었습니다."
);
onBack();
} catch (error) {
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
};
const handleFieldChange = (
field: keyof ClientFormData,
value: string | boolean
) => {
setFormData({ ...formData, [field]: value });
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
return (
<ResponsiveFormTemplate
title={editingClient ? "거래처 수정" : "거래처 등록"}
description="거래처 정보를 입력하세요"
icon={Building2}
onSave={handleSubmit}
onCancel={onBack}
saveLabel={editingClient ? "수정" : "등록"}
isEditMode={!!editingClient}
saveLoading={isSaving || isLoading}
saveDisabled={isSaving || isLoading}
maxWidth="2xl"
>
{/* 1. 기본 정보 */}
<FormSection
title="기본 정보"
description="거래처의 기본 정보를 입력하세요"
icon={Building2}
>
<FormFieldGrid columns={2}>
<FormField
label="사업자등록번호"
required
error={errors.businessNo}
htmlFor="businessNo"
>
<Input
id="businessNo"
placeholder="10자리 숫자"
value={formData.businessNo}
onChange={(e) => handleFieldChange("businessNo", e.target.value)}
/>
</FormField>
<FormField
label="거래처 코드"
htmlFor="clientCode"
helpText="자동 생성됩니다"
>
<Input
id="clientCode"
placeholder="자동생성"
value={formData.clientCode || ""}
disabled
/>
</FormField>
</FormFieldGrid>
<FormFieldGrid columns={2}>
<FormField
label="거래처명"
required
error={errors.name}
htmlFor="name"
>
<Input
id="name"
placeholder="거래처명 입력"
value={formData.name}
onChange={(e) => handleFieldChange("name", e.target.value)}
/>
</FormField>
<FormField
label="대표자명"
required
error={errors.representative}
htmlFor="representative"
>
<Input
id="representative"
placeholder="대표자명 입력"
value={formData.representative}
onChange={(e) =>
handleFieldChange("representative", e.target.value)
}
/>
</FormField>
</FormFieldGrid>
<FormField label="거래처 유형" required type="custom">
<RadioGroup
value={formData.clientType}
onValueChange={(value) =>
handleFieldChange("clientType", value as ClientType)
}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="매입" id="type-purchase" />
<Label htmlFor="type-purchase"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="매출" id="type-sales" />
<Label htmlFor="type-sales"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="매입매출" id="type-both" />
<Label htmlFor="type-both"></Label>
</div>
</RadioGroup>
</FormField>
<FormFieldGrid columns={2}>
<FormField label="업태" htmlFor="businessType">
<Input
id="businessType"
placeholder="제조업, 도소매업 등"
value={formData.businessType}
onChange={(e) =>
handleFieldChange("businessType", e.target.value)
}
/>
</FormField>
<FormField label="종목" htmlFor="businessItem">
<Input
id="businessItem"
placeholder="철강, 건설 등"
value={formData.businessItem}
onChange={(e) =>
handleFieldChange("businessItem", e.target.value)
}
/>
</FormField>
</FormFieldGrid>
</FormSection>
{/* 2. 연락처 정보 */}
<FormSection
title="연락처 정보"
description="거래처의 연락처 정보를 입력하세요"
icon={Phone}
>
<FormField label="주소" htmlFor="address">
<Input
id="address"
placeholder="주소 입력"
value={formData.address}
onChange={(e) => handleFieldChange("address", e.target.value)}
/>
</FormField>
<FormFieldGrid columns={3}>
<FormField label="전화번호" error={errors.phone} htmlFor="phone">
<Input
id="phone"
placeholder="02-1234-5678"
value={formData.phone}
onChange={(e) => handleFieldChange("phone", e.target.value)}
/>
</FormField>
<FormField label="모바일" htmlFor="mobile">
<Input
id="mobile"
placeholder="010-1234-5678"
value={formData.mobile}
onChange={(e) => handleFieldChange("mobile", e.target.value)}
/>
</FormField>
<FormField label="팩스" htmlFor="fax">
<Input
id="fax"
placeholder="02-1234-5678"
value={formData.fax}
onChange={(e) => handleFieldChange("fax", e.target.value)}
/>
</FormField>
</FormFieldGrid>
<FormField label="이메일" error={errors.email} htmlFor="email">
<Input
id="email"
type="email"
placeholder="example@company.com"
value={formData.email}
onChange={(e) => handleFieldChange("email", e.target.value)}
/>
</FormField>
</FormSection>
{/* 3. 담당자 정보 */}
<FormSection
title="담당자 정보"
description="거래처 담당자 정보를 입력하세요"
icon={UserCircle}
>
<FormFieldGrid columns={2}>
<FormField label="담당자명" htmlFor="managerName">
<Input
id="managerName"
placeholder="담당자명 입력"
value={formData.managerName}
onChange={(e) => handleFieldChange("managerName", e.target.value)}
/>
</FormField>
<FormField label="담당자 전화" htmlFor="managerTel">
<Input
id="managerTel"
placeholder="010-1234-5678"
value={formData.managerTel}
onChange={(e) => handleFieldChange("managerTel", e.target.value)}
/>
</FormField>
</FormFieldGrid>
<FormField label="시스템 관리자" htmlFor="systemManager">
<Input
id="systemManager"
placeholder="시스템 관리자명"
value={formData.systemManager}
onChange={(e) =>
handleFieldChange("systemManager", e.target.value)
}
/>
</FormField>
</FormSection>
{/* 4. 발주처 설정 */}
<FormSection
title="발주처 설정"
description="발주처로 사용할 경우 계정 정보를 입력하세요"
icon={CreditCard}
>
<FormFieldGrid columns={2}>
<FormField label="계정 ID" htmlFor="accountId">
<Input
id="accountId"
placeholder="계정 ID"
value={formData.accountId}
onChange={(e) => handleFieldChange("accountId", e.target.value)}
/>
</FormField>
<FormField label="비밀번호" htmlFor="accountPassword">
<Input
id="accountPassword"
type="password"
placeholder="비밀번호"
value={formData.accountPassword}
onChange={(e) =>
handleFieldChange("accountPassword", e.target.value)
}
/>
</FormField>
</FormFieldGrid>
<FormFieldGrid columns={2}>
<FormField label="매입 결제일" htmlFor="purchasePaymentDay">
<Select
value={formData.purchasePaymentDay}
onValueChange={(value) =>
handleFieldChange("purchasePaymentDay", value)
}
>
<SelectTrigger id="purchasePaymentDay">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="말일"></SelectItem>
<SelectItem value="익월 10일"> 10</SelectItem>
<SelectItem value="익월 15일"> 15</SelectItem>
<SelectItem value="익월 20일"> 20</SelectItem>
<SelectItem value="익월 25일"> 25</SelectItem>
<SelectItem value="익월 말일"> </SelectItem>
</SelectContent>
</Select>
</FormField>
<FormField label="매출 결제일" htmlFor="salesPaymentDay">
<Select
value={formData.salesPaymentDay}
onValueChange={(value) =>
handleFieldChange("salesPaymentDay", value)
}
>
<SelectTrigger id="salesPaymentDay">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="말일"></SelectItem>
<SelectItem value="익월 10일"> 10</SelectItem>
<SelectItem value="익월 15일"> 15</SelectItem>
<SelectItem value="익월 20일"> 20</SelectItem>
<SelectItem value="익월 25일"> 25</SelectItem>
<SelectItem value="익월 말일"> </SelectItem>
</SelectContent>
</Select>
</FormField>
</FormFieldGrid>
</FormSection>
{/* 5. 약정 세금 */}
<FormSection
title="약정 세금"
description="세금 약정이 있는 경우 입력하세요"
icon={Calculator}
>
<FormField label="약정 여부" type="custom">
<div className="flex items-center space-x-2">
<Checkbox
id="taxAgreement"
checked={formData.taxAgreement}
onCheckedChange={(checked) =>
handleFieldChange("taxAgreement", checked as boolean)
}
/>
<Label htmlFor="taxAgreement"> </Label>
</div>
</FormField>
{formData.taxAgreement && (
<>
<FormField label="약정 금액" htmlFor="taxAmount">
<Input
id="taxAmount"
type="number"
placeholder="약정 금액"
value={formData.taxAmount}
onChange={(e) => handleFieldChange("taxAmount", e.target.value)}
/>
</FormField>
<FormFieldGrid columns={2}>
<FormField label="약정 시작일" htmlFor="taxStartDate">
<Input
id="taxStartDate"
type="date"
value={formData.taxStartDate}
onChange={(e) =>
handleFieldChange("taxStartDate", e.target.value)
}
/>
</FormField>
<FormField label="약정 종료일" htmlFor="taxEndDate">
<Input
id="taxEndDate"
type="date"
value={formData.taxEndDate}
onChange={(e) =>
handleFieldChange("taxEndDate", e.target.value)
}
/>
</FormField>
</FormFieldGrid>
</>
)}
</FormSection>
{/* 6. 악성채권 */}
<FormSection
title="악성채권 정보"
description="악성채권이 있는 경우 입력하세요"
icon={AlertTriangle}
>
<FormField label="악성채권 여부" type="custom">
<div className="flex items-center space-x-2">
<Checkbox
id="badDebt"
checked={formData.badDebt}
onCheckedChange={(checked) =>
handleFieldChange("badDebt", checked as boolean)
}
/>
<Label htmlFor="badDebt"> </Label>
</div>
</FormField>
{formData.badDebt && (
<>
<FormField label="악성채권 금액" htmlFor="badDebtAmount">
<Input
id="badDebtAmount"
type="number"
placeholder="채권 금액"
value={formData.badDebtAmount}
onChange={(e) =>
handleFieldChange("badDebtAmount", e.target.value)
}
/>
</FormField>
<FormFieldGrid columns={2}>
<FormField label="채권 발생일" htmlFor="badDebtReceiveDate">
<Input
id="badDebtReceiveDate"
type="date"
value={formData.badDebtReceiveDate}
onChange={(e) =>
handleFieldChange("badDebtReceiveDate", e.target.value)
}
/>
</FormField>
<FormField label="채권 만료일" htmlFor="badDebtEndDate">
<Input
id="badDebtEndDate"
type="date"
value={formData.badDebtEndDate}
onChange={(e) =>
handleFieldChange("badDebtEndDate", e.target.value)
}
/>
</FormField>
</FormFieldGrid>
<FormField label="진행 상태" htmlFor="badDebtProgress">
<Select
value={formData.badDebtProgress}
onValueChange={(value) =>
handleFieldChange("badDebtProgress", value as BadDebtProgress)
}
>
<SelectTrigger id="badDebtProgress">
<SelectValue placeholder="진행 상태 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="협의중"></SelectItem>
<SelectItem value="소송중"></SelectItem>
<SelectItem value="회수완료"></SelectItem>
<SelectItem value="대손처리"></SelectItem>
</SelectContent>
</Select>
</FormField>
</>
)}
</FormSection>
{/* 7. 기타 */}
<FormSection
title="기타 정보"
description="추가 정보를 입력하세요"
icon={FileText}
>
<FormField label="메모" htmlFor="memo">
<Textarea
id="memo"
placeholder="메모 입력"
value={formData.memo}
onChange={(e) => handleFieldChange("memo", e.target.value)}
rows={4}
/>
</FormField>
<FormField label="상태" type="custom">
<RadioGroup
value={formData.isActive ? "활성" : "비활성"}
onValueChange={(value) =>
handleFieldChange("isActive", value === "활성")
}
className="flex gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="활성" id="status-active" />
<Label htmlFor="status-active"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="비활성" id="status-inactive" />
<Label htmlFor="status-inactive"></Label>
</div>
</RadioGroup>
</FormField>
</FormSection>
</ResponsiveFormTemplate>
);
}