feat(WEB): 자재/영업/품질 모듈 기능 개선 및 문서 컴포넌트 추가

- 입고관리: 상세/목록 UI 개선, actions 로직 강화
- 재고현황: 상세/목록 개선, StockAuditModal 신규 추가
- 영업주문관리: 페이지 구조 개선, OrderSalesDetailEdit 기능 강화
- 주문: OrderRegistration 개선, SalesOrderDocument 신규 추가
- 견적: QuoteTransactionModal 기능 개선
- 품질: InspectionModalV2, ImportInspectionDocument 대폭 개선
- UniversalListPage: 템플릿 기능 확장

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-28 21:15:25 +09:00
parent 79b39a3ef6
commit 1f640622e0
23 changed files with 4683 additions and 1946 deletions

View File

@@ -6,8 +6,7 @@
* IntegratedDetailTemplate 마이그레이션 (2026-01-20)
* - 견적 불러오기 섹션
* - 기본 정보 섹션
* - 수주/배송 정보 섹션
* - 수신처 주소 섹션
* - 수주/배송 정보 섹션 (주소 포함)
* - 비고 섹션
* - 품목 내역 섹션
*/
@@ -46,8 +45,6 @@ import {
X,
Plus,
Trash2,
Info,
MapPin,
Truck,
Package,
MessageSquare,
@@ -133,6 +130,8 @@ const DELIVERY_METHODS = [
{ value: "direct", label: "직접배차" },
{ value: "pickup", label: "상차" },
{ value: "courier", label: "택배" },
{ value: "self", label: "직접수령" },
{ value: "freight", label: "화물" },
];
// 운임비용 옵션
@@ -162,11 +161,11 @@ interface FieldErrors {
// 필드명 한글 매핑
const FIELD_NAME_MAP: Record<string, string> = {
clientName: "주처",
clientName: "주처",
siteName: "현장명",
deliveryRequestDate: "납품요청일",
receiver: "수신(반장/업체)",
receiverContact: "수신처 연락처",
receiver: "수신",
receiverContact: "수신처",
items: "품목 내역",
};
@@ -456,13 +455,34 @@ export function OrderRegistration({
{/* 기본 정보 섹션 */}
<FormSection
title="기본 정보"
description="주처 및 현장 정보를 입력하세요"
description="주처 및 현장 정보를 입력하세요"
icon={FileText}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 첫 번째 줄: 로트번호, 접수일, 수주처, 현장명 */}
<div className="space-y-2">
<Label></Label>
<Input
value=""
disabled
className="bg-muted"
placeholder="자동 생성"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value=""
disabled
className="bg-muted"
placeholder="자동 생성"
/>
</div>
<div className="space-y-2">
<Label>
<span className="text-red-500">*</span>
<span className="text-red-500">*</span>
</Label>
<Select
value={form.clientId}
@@ -478,8 +498,8 @@ export function OrderRegistration({
disabled={!!form.selectedQuotation || isClientsLoading}
>
<SelectTrigger className={cn(fieldErrors.clientName && "border-red-500")}>
<SelectValue placeholder={isClientsLoading ? "불러오는 중..." : "주처 선택"}>
{form.clientName || (isClientsLoading ? "불러오는 중..." : "주처 선택")}
<SelectValue placeholder={isClientsLoading ? "불러오는 중..." : "주처 선택"}>
{form.clientName || (isClientsLoading ? "불러오는 중..." : "주처 선택")}
</SelectValue>
</SelectTrigger>
<SelectContent>
@@ -514,6 +534,7 @@ export function OrderRegistration({
)}
</div>
{/* 두 번째 줄: 담당자, 연락처, 상태 */}
<div className="space-y-2">
<Label></Label>
<Input
@@ -535,6 +556,16 @@ export function OrderRegistration({
}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Input
value=""
disabled
className="bg-muted"
placeholder="자동 생성"
/>
</div>
</div>
</FormSection>
@@ -544,93 +575,55 @@ export function OrderRegistration({
description="출고 및 배송 정보를 입력하세요"
icon={Truck}
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 출고예정일 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 첫 번째 줄: 수주일, 납품요청일, 출고예정일, 배송방식 */}
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Input
type="date"
value={form.expectedShipDate}
onChange={(e) =>
setForm((prev) => ({
...prev,
expectedShipDate: e.target.value,
}))
}
disabled={form.expectedShipDateUndecided}
className="flex-1"
/>
<div className="flex items-center gap-2">
<Checkbox
id="expectedShipDateUndecided"
checked={form.expectedShipDateUndecided}
onCheckedChange={(checked) =>
setForm((prev) => ({
...prev,
expectedShipDateUndecided: checked as boolean,
expectedShipDate: checked ? "" : prev.expectedShipDate,
}))
}
/>
<Label
htmlFor="expectedShipDateUndecided"
className="text-sm font-normal"
>
</Label>
</div>
</div>
<Label></Label>
<Input
value=""
disabled
className="bg-muted"
placeholder="자동 생성"
/>
</div>
{/* 납품요청일 */}
<div className="space-y-2">
<Label>
<span className="text-red-500">*</span>
</Label>
<div className="flex items-center gap-4">
<Input
type="date"
value={form.deliveryRequestDate}
onChange={(e) => {
setForm((prev) => ({
...prev,
deliveryRequestDate: e.target.value,
}));
clearFieldError("deliveryRequestDate");
}}
disabled={form.deliveryRequestDateUndecided}
className={cn("flex-1", fieldErrors.deliveryRequestDate && "border-red-500")}
/>
<div className="flex items-center gap-2">
<Checkbox
id="deliveryRequestDateUndecided"
checked={form.deliveryRequestDateUndecided}
onCheckedChange={(checked) => {
setForm((prev) => ({
...prev,
deliveryRequestDateUndecided: checked as boolean,
deliveryRequestDate: checked
? ""
: prev.deliveryRequestDate,
}));
if (checked) clearFieldError("deliveryRequestDate");
}}
/>
<Label
htmlFor="deliveryRequestDateUndecided"
className="text-sm font-normal"
>
</Label>
</div>
</div>
<Input
type="date"
value={form.deliveryRequestDate}
onChange={(e) => {
setForm((prev) => ({
...prev,
deliveryRequestDate: e.target.value,
}));
clearFieldError("deliveryRequestDate");
}}
disabled={form.deliveryRequestDateUndecided}
className={cn(fieldErrors.deliveryRequestDate && "border-red-500")}
/>
{fieldErrors.deliveryRequestDate && (
<p className="text-sm text-red-500">{fieldErrors.deliveryRequestDate}</p>
)}
</div>
{/* 배송방식 */}
<div className="space-y-2">
<Label></Label>
<Input
type="date"
value={form.expectedShipDate}
onChange={(e) =>
setForm((prev) => ({
...prev,
expectedShipDate: e.target.value,
}))
}
disabled={form.expectedShipDateUndecided}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select
@@ -640,7 +633,7 @@ export function OrderRegistration({
}
>
<SelectTrigger>
<SelectValue placeholder="배송방식 선택" />
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{DELIVERY_METHODS.map((method) => (
@@ -652,7 +645,7 @@ export function OrderRegistration({
</Select>
</div>
{/* 운임비용 */}
{/* 두 번째 줄: 운임비용, 수신자, 수신처 */}
<div className="space-y-2">
<Label></Label>
<Select
@@ -662,7 +655,7 @@ export function OrderRegistration({
}
>
<SelectTrigger>
<SelectValue placeholder="운임비용 선택" />
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{SHIPPING_COSTS.map((cost) => (
@@ -674,10 +667,9 @@ export function OrderRegistration({
</Select>
</div>
{/* 수신(반장/업체) */}
<div className="space-y-2">
<Label>
(/) <span className="text-red-500">*</span>
<span className="text-red-500">*</span>
</Label>
<Input
placeholder="수신자명 입력"
@@ -693,18 +685,17 @@ export function OrderRegistration({
)}
</div>
{/* 수신처 연락처 */}
<div className="space-y-2">
<Label>
<span className="text-red-500">*</span>
<span className="text-red-500">*</span>
</Label>
<PhoneInput
placeholder="010-0000-0000"
<Input
placeholder="수신처 입력"
value={form.receiverContact}
onChange={(value) => {
onChange={(e) => {
setForm((prev) => ({
...prev,
receiverContact: value,
receiverContact: e.target.value,
}));
clearFieldError("receiverContact");
}}
@@ -714,43 +705,32 @@ export function OrderRegistration({
<p className="text-sm text-red-500">{fieldErrors.receiverContact}</p>
)}
</div>
</div>
</FormSection>
{/* 수신처 주소 섹션 */}
<FormSection
title="수신처 주소"
description="배송지 주소를 입력하세요"
icon={MapPin}
>
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="우편번호"
value={form.zipCode}
onChange={(e) =>
setForm((prev) => ({ ...prev, zipCode: e.target.value }))
}
className="w-32"
/>
<Button variant="outline" type="button" onClick={openPostcode}>
</Button>
{/* 주소 - 전체 너비 사용 */}
<div className="space-y-2 md:col-span-4">
<Label></Label>
<div className="flex gap-2">
<Button variant="outline" type="button" onClick={openPostcode}>
</Button>
<Input
placeholder="우편번호"
value={form.zipCode}
onChange={(e) =>
setForm((prev) => ({ ...prev, zipCode: e.target.value }))
}
className="w-32"
/>
<Input
placeholder="주소"
value={form.address}
onChange={(e) =>
setForm((prev) => ({ ...prev, address: e.target.value }))
}
className="flex-1"
/>
</div>
</div>
<Input
placeholder="기본 주소"
value={form.address}
onChange={(e) =>
setForm((prev) => ({ ...prev, address: e.target.value }))
}
/>
<Input
placeholder="상세 주소 입력"
value={form.addressDetail}
onChange={(e) =>
setForm((prev) => ({ ...prev, addressDetail: e.target.value }))
}
/>
</div>
</FormSection>