feat(WEB): 입력 컴포넌트 공통화 및 UI 개선

- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가
- MobileCard 컴포넌트 통합 (ListMobileCard 제거)
- IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈)
- IntegratedDetailTemplate 타이틀 중복 수정
- 문서 시스템 컴포넌트 추가
- 헤더 벨 아이콘 포커스 스타일 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-21 20:56:17 +09:00
parent cfa72fe19b
commit 835c06ce94
190 changed files with 8575 additions and 2354 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
}