feat: 모바일 반응형 UI 개선 및 공휴일/일정 시스템 통합

- MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선
- DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가
- useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱
- 전 도메인 날짜 필드 DatePicker 표준화
- 생산대시보드/작업지시/견적서/주문관리 모바일 호환성 강화
- 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등)
- 달력 일정 관리 API 연동 및 대량 등록 다이얼로그 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-26 21:28:23 +09:00
parent 3f0a3584ec
commit 13d27553b9
107 changed files with 1703 additions and 970 deletions

View File

@@ -323,9 +323,9 @@ export function MaterialInputModal({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!max-w-3xl p-0 gap-0">
<DialogContent className="!max-w-3xl p-0 gap-0 max-h-[85vh] flex flex-col">
{/* 헤더 */}
<DialogHeader className="p-6 pb-4">
<DialogHeader className="p-6 pb-4 shrink-0">
<DialogTitle className="text-xl font-semibold">
{workOrderItemName ? ` - ${workOrderItemName}` : ''}
</DialogTitle>
@@ -334,7 +334,7 @@ export function MaterialInputModal({
</p>
</DialogHeader>
<div className="px-6 pb-6 space-y-4">
<div className="px-6 pb-6 space-y-4 flex-1 min-h-0 flex flex-col">
{/* 자재 목록 */}
{isLoading ? (
<ContentSkeleton type="table" rows={4} />
@@ -343,7 +343,7 @@ export function MaterialInputModal({
.
</div>
) : (
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
<div className="space-y-4 flex-1 overflow-y-auto min-h-0">
{materialGroups.map((group) => {
const groupAllocated = group.lots.reduce(
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
@@ -355,8 +355,8 @@ export function MaterialInputModal({
return (
<div key={group.groupKey} className="border rounded-lg overflow-hidden">
{/* 품목 그룹 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b">
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center justify-between gap-1.5 px-4 py-2.5 bg-gray-50 border-b">
<div className="flex items-center gap-2 flex-wrap min-w-0">
{group.category && (
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
group.category === 'guideRail' ? 'bg-blue-100 text-blue-700' :
@@ -381,7 +381,7 @@ export function MaterialInputModal({
</span>
)}
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-gray-500">
{group.alreadyInputted > 0 ? (
<>
@@ -433,6 +433,7 @@ export function MaterialInputModal({
</div>
{/* 로트 테이블 */}
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@@ -509,6 +510,7 @@ export function MaterialInputModal({
})}
</TableBody>
</Table>
</div>
</div>
);
})}
@@ -516,7 +518,7 @@ export function MaterialInputModal({
)}
{/* 버튼 영역 */}
<div className="flex gap-3">
<div className="flex gap-3 shrink-0">
<Button
variant="outline"
onClick={resetAndClose}

View File

@@ -14,7 +14,7 @@
*/
import { useState, useCallback, memo } from 'react';
import { ChevronDown, ChevronUp, Pencil, Trash2, ImageIcon } from 'lucide-react';
import { ChevronDown, ChevronUp, SquarePen, Trash2, ImageIcon } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -89,11 +89,12 @@ export const WorkItemCard = memo(function WorkItemCard({
{/* 제작 사이즈 (재공품은 숨김) */}
{!item.isWip && (
<div className="flex items-center gap-1.5 text-sm text-gray-700">
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-sm text-gray-700">
<span className="text-gray-500"> </span>
<span className="font-medium">
{formatNumber(item.width)} X {formatNumber(item.height)} mm
</span>
<span className="text-gray-500"></span>
<span className="font-medium text-gray-900">{item.quantity}</span>
</div>
)}
@@ -216,7 +217,7 @@ export const WorkItemCard = memo(function WorkItemCard({
className="h-7 w-7 p-0"
onClick={() => onEditMaterial(item.id, mat)}
>
<Pencil className="h-3.5 w-3.5 text-gray-500" />
<SquarePen className="h-3.5 w-3.5 text-gray-500" />
</Button>
<Button
variant="ghost"
@@ -264,7 +265,7 @@ function SlatExtraInfo({
jointBar?: number;
}) {
return (
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
{length != null && length > 0 && (
<Badge variant="outline" className="text-xs px-2.5 py-1 border-gray-300">
{formatNumber(length)}mm

View File

@@ -1383,7 +1383,7 @@ export default function WorkerScreen() {
return (
<PageLayout>
<div className="pb-20">
<div className="pb-20 overflow-x-hidden">
{/* 완료 토스트 */}
{toastInfo && <CompletionToast info={toastInfo} />}
@@ -1403,21 +1403,23 @@ export default function WorkerScreen() {
value={activeTab}
onValueChange={(v) => setActiveTab(v)}
>
<TabsList className="w-full">
{processTabs.length > 0 ? (
processTabs.map((proc) => (
<TabsTrigger key={proc.id} value={proc.id} className="flex-1">
{proc.processName}
</TabsTrigger>
))
) : (
(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
<TabsTrigger key={tab} value={tab} className="flex-1">
{PROCESS_TAB_LABELS[tab]}
</TabsTrigger>
))
)}
</TabsList>
<div className="overflow-x-auto -mx-3 px-3 md:mx-0 md:px-0">
<TabsList className="w-max md:w-full">
{processTabs.length > 0 ? (
processTabs.map((proc) => (
<TabsTrigger key={proc.id} value={proc.id} className="px-4 md:flex-1">
{proc.processName}
</TabsTrigger>
))
) : (
(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
<TabsTrigger key={tab} value={tab} className="px-4 md:flex-1">
{PROCESS_TAB_LABELS[tab]}
</TabsTrigger>
))
)}
</TabsList>
</div>
{(processTabs.length > 0
? processTabs.map((p) => p.id)
@@ -1508,7 +1510,7 @@ export default function WorkerScreen() {
<Card>
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3"> </h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3">
<InfoField label="수주일" value={orderInfo?.orderDate} />
<InfoField label="수주로트" value={orderInfo?.salesOrderNo} />
<InfoField label="현장명" value={orderInfo?.siteName} />
@@ -1631,14 +1633,14 @@ export default function WorkerScreen() {
{/* 하단 고정 버튼 */}
{(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && (
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[113px]' : 'md:left-[304px]'}`}>
<div className="flex gap-3">
<div className={`fixed bottom-4 left-3 right-3 px-3 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:left-auto md:right-[24px] md:px-6 ${sidebarCollapsed ? 'md:left-[113px]' : 'md:left-[304px]'}`}>
<div className="flex gap-2 md:gap-3">
{hasWipItems ? (
<Button
onClick={handleInspection}
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
>
</Button>
) : (
<>
@@ -1646,7 +1648,7 @@ export default function WorkerScreen() {
<Button
variant="outline"
onClick={handleWorkLog}
className="flex-1 py-6 text-base font-medium"
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
>
</Button>
@@ -1654,7 +1656,7 @@ export default function WorkerScreen() {
{activeProcessSettings.hasDocumentTemplate && (
<Button
onClick={handleInspection}
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
>
</Button>