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

@@ -109,20 +109,16 @@ export default function ProductForm({
)}
</div>
<div className="md:col-span-2">
<div>
<Label> ()</Label>
<Input
value={(() => {
const pName = productName || '';
const iName = getValues('itemName') || '';
return pName && iName ? `${pName}-${iName}` : '';
})()}
value={getValues('itemName') || ''}
disabled
className="bg-muted text-muted-foreground"
placeholder="상품명과 품목명 입력면 자동으로 생성됩니다"
placeholder="품목명 입력면 자동으로 동일하게 생성됩니다"
/>
<p className="text-xs text-muted-foreground mt-1">
* '상품명-품목명'
*
</p>
</div>
@@ -159,6 +155,15 @@ export default function ProductForm({
*
</p>
</div>
<div>
<Label></Label>
<Input
placeholder="비고 사항을 입력하세요"
value={remarks}
onChange={(e) => setRemarks(e.target.value)}
/>
</div>
</>
);
}
@@ -199,7 +204,7 @@ export function ProductCertificationSection({
</div>
{/* 인정번호, 유효기간, 파일 업로드, 비고 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="certificationNumber"></Label>
<Input
@@ -232,20 +237,28 @@ export function ProductCertificationSection({
{/* 시방서 파일 */}
<div className="space-y-2">
<Label> (PDF, DOCX, HWP, JPG, PNG / 20MB)</Label>
<div className="flex gap-2">
<Input
type="file"
accept=".pdf,.docx,.hwp,.jpg,.jpeg,.png"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setSpecificationFile(file);
}
}}
className="flex-1"
disabled={isSubmitting}
/>
<Label> (PDF)</Label>
<div className="flex items-center gap-2">
<label className="cursor-pointer">
<span className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium border rounded-md bg-background hover:bg-accent hover:text-accent-foreground">
</span>
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setSpecificationFile(file);
}
}}
className="hidden"
disabled={isSubmitting}
/>
</label>
<span className="text-sm text-muted-foreground">
{specificationFile ? specificationFile.name : '선택된 파일 없음'}
</span>
{specificationFile && (
<Button
type="button"
@@ -253,34 +266,38 @@ export function ProductCertificationSection({
size="sm"
onClick={() => setSpecificationFile(null)}
disabled={isSubmitting}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{specificationFile && (
<p className="text-xs text-muted-foreground mt-1">
: {specificationFile.name}
</p>
)}
</div>
{/* 인정서 파일 */}
<div className="space-y-2">
<Label> (PDF, DOCX, HWP, JPG, PNG / 20MB)</Label>
<div className="flex gap-2">
<Input
type="file"
accept=".pdf,.docx,.hwp,.jpg,.jpeg,.png"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setCertificationFile(file);
}
}}
className="flex-1"
disabled={isSubmitting}
/>
<Label> (PDF)</Label>
<div className="flex items-center gap-2">
<label className="cursor-pointer">
<span className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium border rounded-md bg-background hover:bg-accent hover:text-accent-foreground">
</span>
<input
type="file"
accept=".pdf"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setCertificationFile(file);
}
}}
className="hidden"
disabled={isSubmitting}
/>
</label>
<span className="text-sm text-muted-foreground">
{certificationFile ? certificationFile.name : '선택된 파일 없음'}
</span>
{certificationFile && (
<Button
type="button"
@@ -288,20 +305,16 @@ export function ProductCertificationSection({
size="sm"
onClick={() => setCertificationFile(null)}
disabled={isSubmitting}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{certificationFile && (
<p className="text-xs text-muted-foreground mt-1">
: {certificationFile.name}
</p>
)}
</div>
{/* 비고 */}
<div className="md:col-span-2">
<div>
<Label></Label>
<Textarea
value={remarks}

View File

@@ -3,6 +3,7 @@
* - 전동개폐기, 모터, 체인 등
*/
import { useMemo } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
@@ -16,6 +17,7 @@ import {
import type { UseFormRegister, UseFormSetValue, FieldErrors } from 'react-hook-form';
import type { CreateItemFormData } from '@/lib/utils/validation';
import { PART_TYPE_CATEGORIES } from '../../constants';
import { generatePurchasedItemCode } from '@/components/items/DynamicItemForm/utils/itemCodeGenerator';
export interface PurchasedPartFormProps {
selectedCategory1: string;
@@ -60,6 +62,18 @@ export default function PurchasedPartForm({
setValue,
errors,
}: PurchasedPartFormProps) {
// 전동개폐기 품목코드 자동생성 (품목명 + 용량 + 전원)
const generatedItemCode = useMemo(() => {
if (selectedCategory1 === 'electric_opener') {
const category = PART_TYPE_CATEGORIES.PURCHASED?.categories.find(
c => c.value === selectedCategory1
);
const itemName = category?.label || '';
return generatePurchasedItemCode(itemName, electricOpenerCapacity, electricOpenerPower);
}
return '';
}, [selectedCategory1, electricOpenerCapacity, electricOpenerPower]);
return (
<>
{/* 품목명 선택 */}
@@ -258,19 +272,21 @@ export default function PurchasedPartForm({
/>
</div>
{/* 품목코드 자동생성 */}
<div className="md:col-span-2">
<Label> ()</Label>
<Input
value=""
disabled
className="bg-muted text-muted-foreground"
placeholder="품목명과 규격이 입력되면 자동으로 생성됩니다"
/>
<p className="text-xs text-muted-foreground mt-1">
* '품목명-규격'
</p>
</div>
{/* 품목코드 자동생성 - 전동개폐기만 표시 */}
{selectedCategory1 === 'electric_opener' && (
<div className="md:col-span-2">
<Label> ()</Label>
<Input
value={generatedItemCode}
disabled
className="bg-muted text-muted-foreground"
placeholder="용량과 전원을 선택하면 자동으로 생성됩니다"
/>
<p className="text-xs text-muted-foreground mt-1">
* '품목명+용량+전원' (: 전동개폐기150KG380V)
</p>
</div>
)}
{/* 품목 상태 */}
<div className="md:col-span-2">