feat(WEB): 입력 컴포넌트 공통화 및 UI 개선
- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가 - MobileCard 컴포넌트 통합 (ListMobileCard 제거) - IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈) - IntegratedDetailTemplate 타이틀 중복 수정 - 문서 시스템 컴포넌트 추가 - 헤더 벨 아이콘 포커스 스타일 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberInput } from "@/components/ui/number-input";
|
||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -216,12 +218,11 @@ export function ItemAddDialog({
|
||||
<Label htmlFor="width">
|
||||
가로 (mm) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
<NumberInput
|
||||
id="width"
|
||||
type="number"
|
||||
placeholder="예: 7260"
|
||||
value={form.width}
|
||||
onChange={(e) => setForm({ ...form, width: e.target.value })}
|
||||
value={parseFloat(form.width) || 0}
|
||||
onChange={(value) => setForm({ ...form, width: String(value ?? 0) })}
|
||||
/>
|
||||
{errors.width && (
|
||||
<p className="text-xs text-red-500">{errors.width}</p>
|
||||
@@ -231,12 +232,11 @@ export function ItemAddDialog({
|
||||
<Label htmlFor="height">
|
||||
세로 (mm) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
<NumberInput
|
||||
id="height"
|
||||
type="number"
|
||||
placeholder="예: 2600"
|
||||
value={form.height}
|
||||
onChange={(e) => setForm({ ...form, height: e.target.value })}
|
||||
value={parseFloat(form.height) || 0}
|
||||
onChange={(value) => setForm({ ...form, height: String(value ?? 0) })}
|
||||
/>
|
||||
{errors.height && (
|
||||
<p className="text-xs text-red-500">{errors.height}</p>
|
||||
@@ -290,12 +290,11 @@ export function ItemAddDialog({
|
||||
{/* 단가 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="unitPrice">단가 (원)</Label>
|
||||
<Input
|
||||
<CurrencyInput
|
||||
id="unitPrice"
|
||||
type="number"
|
||||
placeholder="예: 8000000"
|
||||
value={form.unitPrice}
|
||||
onChange={(e) => setForm({ ...form, unitPrice: e.target.value })}
|
||||
value={parseFloat(form.unitPrice) || 0}
|
||||
onChange={(value) => setForm({ ...form, unitPrice: String(value ?? 0) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,9 @@ import { useClientList } from "@/hooks/useClientList";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { QuantityInput } from "@/components/ui/quantity-input";
|
||||
import { NumberInput } from "@/components/ui/number-input";
|
||||
import { PhoneInput } from "@/components/ui/phone-input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -512,11 +515,11 @@ export function OrderRegistration({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>연락처</Label>
|
||||
<Input
|
||||
<PhoneInput
|
||||
placeholder="010-0000-0000"
|
||||
value={form.contact}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, contact: e.target.value }))
|
||||
onChange={(value) =>
|
||||
setForm((prev) => ({ ...prev, contact: value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -683,13 +686,13 @@ export function OrderRegistration({
|
||||
<Label>
|
||||
수신처 연락처 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
<PhoneInput
|
||||
placeholder="010-0000-0000"
|
||||
value={form.receiverContact}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
receiverContact: e.target.value,
|
||||
receiverContact: value,
|
||||
}));
|
||||
clearFieldError("receiverContact");
|
||||
}}
|
||||
@@ -801,14 +804,13 @@ export function OrderRegistration({
|
||||
<TableCell>{item.itemName}</TableCell>
|
||||
<TableCell>{item.spec}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Input
|
||||
type="number"
|
||||
<QuantityInput
|
||||
min={1}
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
onChange={(value) =>
|
||||
handleQuantityChange(
|
||||
item.id,
|
||||
Number(e.target.value) || 1
|
||||
value ?? 1
|
||||
)
|
||||
}
|
||||
className="w-16 text-center"
|
||||
@@ -856,17 +858,17 @@ export function OrderRegistration({
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">할인율(%):</span>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
max={100}
|
||||
value={form.discountRate}
|
||||
onChange={(e) =>
|
||||
onChange={(value) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
discountRate: Number(e.target.value) || 0,
|
||||
discountRate: value ?? 0,
|
||||
}))
|
||||
}
|
||||
allowDecimal
|
||||
className="w-20 text-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { PhoneInput } from "@/components/ui/phone-input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -424,10 +425,10 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
|
||||
<Label>
|
||||
수신처 연락처 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
<PhoneInput
|
||||
value={form.receiverContact}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, receiverContact: e.target.value })
|
||||
onChange={(value) =>
|
||||
setForm({ ...form, receiverContact: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,28 +3,14 @@
|
||||
/**
|
||||
* 수주 문서 모달 컴포넌트
|
||||
* - 계약서, 거래명세서, 발주서를 모달 형태로 표시
|
||||
* - DocumentViewer 시스템 사용
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
VisuallyHidden,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
X as XIcon,
|
||||
Printer,
|
||||
Share2,
|
||||
FileDown,
|
||||
Mail,
|
||||
Phone,
|
||||
} from "lucide-react";
|
||||
import { DocumentViewer } from "@/components/document-system";
|
||||
import { ContractDocument } from "./ContractDocument";
|
||||
import { TransactionDocument } from "./TransactionDocument";
|
||||
import { PurchaseOrderDocument } from "./PurchaseOrderDocument";
|
||||
import { printArea } from "@/lib/print-utils";
|
||||
import { OrderItem } from "../actions";
|
||||
import { getCompanyInfo } from "@/components/settings/CompanyInfoManagement/actions";
|
||||
|
||||
@@ -116,26 +102,6 @@ export function OrderDocumentModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
printArea({ title: `${getDocumentTitle()} 인쇄` });
|
||||
};
|
||||
|
||||
const handleSharePdf = () => {
|
||||
console.log("PDF 다운로드");
|
||||
};
|
||||
|
||||
const handleShareEmail = () => {
|
||||
console.log("이메일 공유");
|
||||
};
|
||||
|
||||
const handleShareFax = () => {
|
||||
console.log("팩스 전송");
|
||||
};
|
||||
|
||||
const handleShareKakao = () => {
|
||||
console.log("카카오톡 공유");
|
||||
};
|
||||
|
||||
const renderDocument = () => {
|
||||
switch (documentType) {
|
||||
case "contract":
|
||||
@@ -194,57 +160,14 @@ export function OrderDocumentModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] p-0 flex flex-col gap-0 overflow-hidden [&>button]:hidden">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>{getDocumentTitle()} 상세</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
|
||||
{/* 헤더 영역 - 고정 (인쇄 시 숨김) */}
|
||||
<div className="print-hidden flex-shrink-0 flex items-center justify-between px-6 py-4 border-b bg-white">
|
||||
<h2 className="text-lg font-semibold">{getDocumentTitle()} 상세</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 버튼 영역 - 고정 (인쇄 시 숨김) */}
|
||||
<div className="print-hidden flex-shrink-0 flex items-center gap-2 px-6 py-3 bg-muted/30 border-b">
|
||||
{/* TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유 */}
|
||||
{/* <Button variant="outline" size="sm" onClick={handleSharePdf}>
|
||||
<FileDown className="h-4 w-4 mr-1" />
|
||||
PDF
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleShareEmail}>
|
||||
<Mail className="h-4 w-4 mr-1" />
|
||||
이메일
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleShareFax}>
|
||||
<Phone className="h-4 w-4 mr-1" />
|
||||
팩스
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleShareKakao}>
|
||||
<Share2 className="h-4 w-4 mr-1" />
|
||||
공유
|
||||
</Button> */}
|
||||
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||
<Printer className="h-4 w-4 mr-1" />
|
||||
인쇄
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 문서 영역 - 스크롤 (인쇄 시 이 영역만 출력) */}
|
||||
<div className="print-area flex-1 overflow-y-auto bg-gray-100 p-6">
|
||||
<div className="max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg">
|
||||
{renderDocument()}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DocumentViewer
|
||||
title={getDocumentTitle()}
|
||||
subtitle={`${getDocumentTitle()} 상세`}
|
||||
preset="readonly"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
{renderDocument()}
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user