feat: 모바일 반응형 UI 개선 및 공휴일/일정 시스템 통합
- MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선 - DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가 - useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱 - 전 도메인 날짜 필드 DatePicker 표준화 (104 files) - 생산대시보드/작업지시 모바일 호환성 강화 - 견적서/주문관리 반응형 그리드 적용 - 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ export default function GiftCertificatesPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && id) {
|
||||
if ((mode === 'edit' || mode === 'view') && id) {
|
||||
setIsLoading(true);
|
||||
getGiftCertificateById(id)
|
||||
.then((result) => {
|
||||
@@ -33,6 +33,17 @@ export default function GiftCertificatesPage() {
|
||||
return <GiftCertificateDetail mode="new" />;
|
||||
}
|
||||
|
||||
if (mode === 'view' && id) {
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={8} />;
|
||||
return (
|
||||
<GiftCertificateDetail
|
||||
mode="view"
|
||||
id={id}
|
||||
initialData={editData ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'edit' && id) {
|
||||
if (isLoading) return <ListPageSkeleton showStats statsCount={4} tableColumns={8} />;
|
||||
return (
|
||||
|
||||
@@ -311,7 +311,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">#{globalIndex}</span>
|
||||
<span className="text-sm text-muted-foreground">{globalIndex}</span>
|
||||
{item.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs">공지</Badge>
|
||||
)}
|
||||
|
||||
@@ -318,7 +318,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">#{globalIndex}</span>
|
||||
<span className="text-sm text-muted-foreground">{globalIndex}</span>
|
||||
{item.isNotice && (
|
||||
<Badge variant="destructive" className="text-xs">공지</Badge>
|
||||
)}
|
||||
|
||||
@@ -502,7 +502,7 @@ export default function CustomerAccountManagementPage() {
|
||||
variant="outline"
|
||||
className="bg-gray-100 text-gray-700 font-mono text-xs"
|
||||
>
|
||||
#{globalIndex}
|
||||
{globalIndex}
|
||||
</Badge>
|
||||
<code className="inline-block text-xs bg-gray-100 text-gray-700 px-2.5 py-0.5 rounded-md font-mono whitespace-nowrap">
|
||||
{customer.code}
|
||||
|
||||
@@ -267,8 +267,8 @@ export default function OrderEditPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 상태 뱃지 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded whitespace-nowrap">
|
||||
{form.lotNumber}
|
||||
</code>
|
||||
{getOrderStatusBadge(form.status)}
|
||||
@@ -283,7 +283,7 @@ export default function OrderEditPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">로트번호</Label>
|
||||
<p className="font-medium">{form.lotNumber}</p>
|
||||
@@ -298,7 +298,7 @@ export default function OrderEditPage() {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">발주처</Label>
|
||||
<p className="font-medium">{form.client}</p>
|
||||
<p className="font-medium truncate">{form.client}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-muted-foreground text-sm">현장명</Label>
|
||||
|
||||
@@ -527,7 +527,7 @@ export default function OrderDetailPage() {
|
||||
<CardTitle className="text-lg">기본 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<InfoItem label="로트번호" value={order.lotNumber} />
|
||||
<InfoItem label="수주일" value={order.orderDate} />
|
||||
<InfoItem label="수주처" value={order.client} />
|
||||
@@ -555,7 +555,7 @@ export default function OrderDetailPage() {
|
||||
<InfoItem label="운임비용" value={order.shippingCostLabel || order.shippingCost} />
|
||||
<InfoItem label="수신자" value={order.receiver} />
|
||||
<InfoItem label="수신처 연락처" value={order.receiverContact} />
|
||||
<div className="md:col-span-2">
|
||||
<div className="col-span-2 md:col-span-4">
|
||||
<p className="text-sm text-muted-foreground">주소</p>
|
||||
<p className="font-medium">{order.address || "-"}</p>
|
||||
</div>
|
||||
@@ -578,10 +578,10 @@ export default function OrderDetailPage() {
|
||||
{/* 제품내용 (아코디언) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">제품내용</CardTitle>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<CardTitle className="text-lg shrink-0">제품내용</CardTitle>
|
||||
{((order.nodes && order.nodes.length > 0) || (order.products && order.products.length > 0)) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -625,27 +625,25 @@ export default function OrderDetailPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleProduct(productKey)}
|
||||
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left"
|
||||
className="w-full flex items-center p-3 sm:p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="shrink-0 w-7 h-7 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
<Package className="h-5 w-5 text-blue-600" />
|
||||
<div>
|
||||
<span className="font-medium">{productName}</span>
|
||||
</div>
|
||||
<Package className="h-5 w-5 text-blue-600 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium break-words">{productName}</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-gray-500">
|
||||
{nodeWidth && nodeHeight && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
({nodeWidth} × {nodeHeight})
|
||||
</span>
|
||||
<span className="whitespace-nowrap">({nodeWidth} × {nodeHeight})</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">부품 {nodeItems.length}개</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
부품 {nodeItems.length}개
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 부품 목록 (확장 시 표시) */}
|
||||
@@ -701,27 +699,25 @@ export default function OrderDetailPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleProduct(productKey)}
|
||||
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left"
|
||||
className="w-full flex items-center p-3 sm:p-4 bg-gray-50 hover:bg-gray-100 transition-colors text-left gap-2"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="shrink-0 w-7 h-7 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-5 w-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 text-gray-500" />
|
||||
)}
|
||||
<Package className="h-5 w-5 text-blue-600" />
|
||||
<div>
|
||||
<span className="font-medium">{product.productName}</span>
|
||||
</div>
|
||||
<Package className="h-5 w-5 text-blue-600 shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium break-words">{product.productName}</div>
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-gray-500">
|
||||
{product.openWidth && product.openHeight && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
({product.openWidth} × {product.openHeight})
|
||||
</span>
|
||||
<span className="whitespace-nowrap">({product.openWidth} × {product.openHeight})</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">부품 {productItems.length}개</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
부품 {productItems.length}개
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 부품 목록 (확장 시 표시) */}
|
||||
@@ -1206,37 +1202,37 @@ export default function OrderDetailPage() {
|
||||
|
||||
{/* 생산지시 되돌리기 다이얼로그 */}
|
||||
<Dialog open={isRevertDialogOpen} onOpenChange={setIsRevertDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogContent className="max-w-md max-h-[85vh] flex flex-col">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RotateCcw className="h-5 w-5 text-amber-600" />
|
||||
생산지시 되돌리기
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 overflow-y-auto flex-1 min-h-0">
|
||||
{/* 수주 정보 박스 */}
|
||||
<div className="border rounded-lg p-4 space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">수주번호</span>
|
||||
<span className="font-medium">{order.lotNumber}</span>
|
||||
<div className="border rounded-lg p-3 space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground whitespace-nowrap shrink-0">수주번호</span>
|
||||
<span className="font-medium truncate">{order.lotNumber}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">발주처</span>
|
||||
<span>{order.client}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground whitespace-nowrap shrink-0">발주처</span>
|
||||
<span className="truncate">{order.client}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">현장명</span>
|
||||
<span>{order.siteName}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground whitespace-nowrap shrink-0">현장명</span>
|
||||
<span className="truncate">{order.siteName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">현재 상태</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground whitespace-nowrap shrink-0">현재 상태</span>
|
||||
{getOrderStatusBadge(order.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 경고 메시지 */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 text-sm space-y-1">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm space-y-1">
|
||||
<p className="font-medium mb-2 text-amber-700">⚠️ 되돌리기 시 삭제되는 데이터</p>
|
||||
<ul className="space-y-1 text-amber-600">
|
||||
<li>• 이 수주에 연결된 모든 작업지시가 삭제됩니다</li>
|
||||
@@ -1253,38 +1249,40 @@ export default function OrderDetailPage() {
|
||||
placeholder="되돌리기 사유를 입력해주세요 (선택)"
|
||||
value={revertReason}
|
||||
onChange={(e) => setRevertReason(e.target.value)}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 확인 안내 */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm">
|
||||
<p className="text-red-700 font-medium">
|
||||
이 작업은 되돌릴 수 없습니다. 정말로 생산지시를 되돌리시겠습니까?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<DialogFooter className="shrink-0 flex-col gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsRevertDialogOpen(false)}
|
||||
className="w-full"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full">
|
||||
{isDev && (
|
||||
<Button
|
||||
onClick={handleForceRevertProduction}
|
||||
variant="destructive"
|
||||
disabled={isForceReverting || isReverting}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isForceReverting ? "처리 중..." : "완전삭제 (DEV)"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleRevertProductionSubmit}
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
className="bg-amber-600 hover:bg-amber-700 w-full sm:w-auto"
|
||||
disabled={isReverting || isForceReverting}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />
|
||||
@@ -1297,7 +1295,7 @@ export default function OrderDetailPage() {
|
||||
|
||||
{/* 수주확정 되돌리기 다이얼로그 */}
|
||||
<Dialog open={isRevertConfirmDialogOpen} onOpenChange={setIsRevertConfirmDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className="max-w-md max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RotateCcw className="h-5 w-5 text-slate-600" />
|
||||
@@ -1305,7 +1303,7 @@ export default function OrderDetailPage() {
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 overflow-y-auto flex-1">
|
||||
{/* 수주 정보 박스 */}
|
||||
<div className="border rounded-lg p-4 space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -613,7 +613,7 @@ function OrderListContent() {
|
||||
variant="outline"
|
||||
className="bg-gray-100 text-gray-700 font-mono text-xs"
|
||||
>
|
||||
#{globalIndex}
|
||||
{globalIndex}
|
||||
</Badge>
|
||||
<code className="inline-block text-xs bg-gray-100 text-gray-700 px-2.5 py-0.5 rounded-md font-mono whitespace-nowrap">
|
||||
{order.lotNumber}
|
||||
|
||||
Reference in New Issue
Block a user