From 13d27553b9f08a5d8a040ad8a2c4107ce318cd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 26 Feb 2026 21:28:23 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20UI=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B3=B5=ED=9C=B4=EC=9D=BC/=EC=9D=BC=EC=A0=95=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선 - DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가 - useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱 - 전 도메인 날짜 필드 DatePicker 표준화 - 생산대시보드/작업지시/견적서/주문관리 모바일 호환성 강화 - 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등) - 달력 일정 관리 API 연동 및 대량 등록 다이얼로그 개선 Co-Authored-By: Claude Opus 4.6 --- ...-02-26] windows-compatibility-checklist.md | 109 +++++++++ .../accounting/gift-certificates/page.tsx | 13 +- .../(protected)/board/[boardCode]/page.tsx | 2 +- .../(protected)/boards/[boardCode]/page.tsx | 2 +- .../client-management-sales-admin/page.tsx | 2 +- .../order-management-sales/[id]/edit/page.tsx | 8 +- .../order-management-sales/[id]/page.tsx | 98 ++++---- .../sales/order-management-sales/page.tsx | 2 +- .../BadDebtCollection/BadDebtDetail.tsx | 4 +- .../TransactionFormModal.tsx | 46 ++-- .../accounting/BillManagement/BillDetail.tsx | 12 +- .../BillManagement/BillManagementClient.tsx | 62 ++--- .../ManualInputModal.tsx | 14 +- .../accounting/DailyReport/actions.ts | 39 ---- .../accounting/DailyReport/index.tsx | 130 ++++++----- .../ExpectedExpenseManagement/index.tsx | 29 ++- .../AccountSubjectSettingModal.tsx | 118 +++++----- .../GeneralJournalEntry/JournalEditModal.tsx | 6 +- .../ManualJournalEntryModal.tsx | 6 +- .../accounting/GeneralJournalEntry/index.tsx | 2 - .../GiftCertificateDetail.tsx | 65 +++--- .../GiftCertificateManagement/actions.ts | 20 +- .../PurchaseManagement/PurchaseDetail.tsx | 212 +++++++++--------- .../PurchaseDetailModal.tsx | 4 +- .../accounting/PurchaseManagement/actions.ts | 3 +- .../accounting/ReceivablesStatus/index.tsx | 148 ++++++++++-- .../SalesManagement/SalesDetail.tsx | 45 ++-- .../accounting/TaxInvoiceIssuance/index.tsx | 4 +- .../TaxInvoiceManagement/CardHistoryModal.tsx | 8 +- .../TaxInvoiceManagement/ManualEntryModal.tsx | 10 +- .../accounting/TaxInvoiceManagement/index.tsx | 23 +- .../VendorLedger/VendorLedgerDetail.tsx | 38 ++-- .../VendorManagement/VendorDetail.tsx | 145 ++++++++++-- .../VendorManagement/VendorDetailClient.tsx | 4 +- .../DocumentCreate/ApprovalLineSection.tsx | 4 +- .../DocumentCreate/ExpenseReportForm.tsx | 4 +- .../DocumentCreate/ReferenceSection.tsx | 4 +- .../board/BoardManagement/index.tsx | 2 +- .../CEODashboard/sections/CalendarSection.tsx | 11 +- .../contract/ContractDetailForm.tsx | 4 +- .../sections/EstimateDetailTableSection.tsx | 4 +- .../sections/EstimateSummarySection.tsx | 4 +- .../HandoverReportDetailForm.tsx | 4 +- .../issue-management/IssueDetailForm.tsx | 4 +- .../item-management/ItemDetailClient.tsx | 4 +- .../management/ConstructionDetailClient.tsx | 15 +- .../OrderManagementUnified.tsx | 2 +- .../construction/partners/PartnerForm.tsx | 4 +- .../site-briefings/SiteBriefingForm.tsx | 4 +- .../ChecklistListClient.tsx | 2 +- .../ScheduleCalendar/ScheduleCalendar.tsx | 10 +- .../EventManagement/EventList.tsx | 2 +- .../InquiryManagement/InquiryList.tsx | 2 +- .../NoticeManagement/NoticeList.tsx | 2 +- .../BulkRegistrationDialog.tsx | 29 ++- .../hr/CalendarManagement/index.tsx | 5 +- .../hr/EmployeeManagement/CSVUploadPage.tsx | 4 +- .../hr/EmployeeManagement/index.tsx | 2 +- src/components/items/ItemTypeSelect.tsx | 2 +- .../ReceivingManagement/ReceivingDetail.tsx | 104 ++++++++- .../ReceivingManagement/ReceivingList.tsx | 2 +- .../material/ReceivingManagement/actions.ts | 6 + .../material/ReceivingManagement/types.ts | 1 + .../material/StockStatus/StockStatusList.tsx | 2 +- .../molecules/ColumnSettingsPopover.tsx | 7 +- src/components/orders/OrderRegistration.tsx | 17 +- .../orders/OrderSalesDetailEdit.tsx | 38 ++-- .../orders/OrderSalesDetailView.tsx | 4 +- .../LineItemsTable/LineItemsTable.tsx | 20 +- src/components/organisms/MobileCard.tsx | 197 +++++++++++----- src/components/organisms/StatCards.tsx | 2 + .../ShipmentManagement/ShipmentCreate.tsx | 14 +- .../ShipmentManagement/ShipmentDetail.tsx | 19 +- .../ShipmentManagement/ShipmentEdit.tsx | 14 +- .../ShipmentManagement/ShipmentList.tsx | 2 +- .../VehicleDispatchEdit.tsx | 6 +- .../VehicleDispatchList.tsx | 2 +- .../PricingTableForm.tsx | 4 +- .../PricingTableListClient.tsx | 2 +- src/components/pricing/PricingListClient.tsx | 8 +- .../process-management/ProcessListClient.tsx | 2 +- .../production/ProductionDashboard/index.tsx | 109 ++++----- .../production/WorkOrders/WorkOrderDetail.tsx | 152 ++++++------- .../production/WorkOrders/WorkOrderEdit.tsx | 12 +- .../production/WorkOrders/WorkOrderList.tsx | 2 +- .../production/WorkResults/WorkResultList.tsx | 2 +- .../WorkerScreen/MaterialInputModal.tsx | 18 +- .../production/WorkerScreen/WorkItemCard.tsx | 9 +- .../production/WorkerScreen/index.tsx | 48 ++-- .../InspectionManagement/InspectionList.tsx | 2 +- .../PerformanceReportList.tsx | 2 +- src/components/quotes/LocationDetailPanel.tsx | 8 +- src/components/quotes/LocationListPanel.tsx | 8 +- src/components/quotes/QuoteFooterBar.tsx | 12 +- .../quotes/QuoteManagementClient.tsx | 2 +- src/components/quotes/QuoteSummaryPanel.tsx | 18 +- .../PaymentHistoryClient.tsx | 32 +-- .../PaymentHistoryManagement/index.tsx | 26 ++- .../settings/PopupManagement/PopupList.tsx | 2 +- src/components/ui/date-picker.tsx | 20 +- src/components/ui/date-range-picker.tsx | 11 +- src/components/ui/date-time-picker.tsx | 92 ++++++++ src/components/ui/multi-select-combobox.tsx | 7 +- src/components/ui/searchable-select.tsx | 7 +- src/components/ui/time-picker.tsx | 7 +- src/hooks/useCalendarScheduleInit.ts | 21 ++ src/layouts/AuthenticatedLayout.tsx | 4 + 107 files changed, 1703 insertions(+), 970 deletions(-) create mode 100644 claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md create mode 100644 src/components/ui/date-time-picker.tsx create mode 100644 src/hooks/useCalendarScheduleInit.ts diff --git a/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md b/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md new file mode 100644 index 00000000..9cab31c1 --- /dev/null +++ b/claudedocs/[FIX-2026-02-26] windows-compatibility-checklist.md @@ -0,0 +1,109 @@ +# Windows 호환성 개선 계획서 + +> 작성일: 2026-02-26 +> 배경: macOS 개발환경 → Windows 공장 PC 사용자 환경 차이로 인한 이슈 +> 상태: 계획 단계 + +--- + +## 완료된 작업 + +- [x] Popover 계열 컴포넌트 Windows 포커스 이슈 수정 (6개 파일) + - `ui/date-picker.tsx` — onPointerDownOutside/onInteractOutside 방어 + - `ui/time-picker.tsx` — 동일 + - `ui/date-range-picker.tsx` — 동일 + - `ui/multi-select-combobox.tsx` — 동일 + - `ui/searchable-select.tsx` — 동일 + - `molecules/ColumnSettingsPopover.tsx` — 동일 +- [x] 삭제 버튼 아이콘 X → Trash2 통일 (23개 파일) + +--- + +## Phase 1: IMPORTANT — 사용자 체감 영향 큰 항목 + +### 1-1. backdrop-filter 성능 최적화 +- **파일**: `src/app/globals.css` (line 269-280) +- **문제**: `backdrop-filter: blur()` 가 Windows GPU에서 비효율적 → 공장 PC에서 스크롤 버벅거림 +- **영향 범위**: `.clean-glass` 클래스 사용하는 전체 레이아웃 (Sidebar, Login 등) +- **수정 방향**: + - `prefers-reduced-motion` / `prefers-reduced-transparency` 미디어쿼리로 분기 + - 성능 낮은 환경에서는 `backdrop-filter: none` + 불투명 배경 대체 +- [ ] globals.css `.clean-glass` 수정 +- [ ] 적용된 컴포넌트에서 시각적 변화 확인 + +### 1-2. 폰트 굵기 렌더링 차이 보정 +- **파일**: `src/app/globals.css` (line 219-235), `src/app/[locale]/layout.tsx` (line 14-19) +- **문제**: Windows 폰트 렌더링이 macOS보다 ~0.5-1.5 weight 더 굵게 표시 (Pretendard 변수 폰트) +- **영향 범위**: 전체 UI 텍스트 +- **수정 방향**: + - `-webkit-font-smoothing: antialiased` 확인 (이미 적용됨) + - `font-weight: 400` 명시적 지정 + - 필요 시 Windows에서 `font-weight: 350` 적용 검토 (변수 폰트이므로 가능) +- [ ] 현재 폰트 설정 확인 +- [ ] Windows에서 시각 비교 테스트 후 보정값 결정 +- [ ] globals.css 수정 + +### 1-3. 스크롤바 동작 차이 +- **파일**: `src/app/globals.css` (line 332-412), `src/layouts/AuthenticatedLayout.tsx` (line 173-197) +- **문제**: macOS는 스크롤바 자동 숨김, Windows는 항상 표시 → 투명→페이드인 방식이 어색 +- **영향 범위**: 모든 스크롤 가능한 영역 +- **수정 방향**: + - 스크롤바 thumb을 기본 살짝 보이게 (`rgba(0,0,0,0.1)`) + - `.is-scrolling` 시 진하게 (`rgba(0,0,0,0.2)`) 유지 + - 너비 8px → 10-12px 로 Windows 기대치에 맞게 조정 검토 +- [ ] globals.css 스크롤바 스타일 수정 +- [ ] Windows에서 시각 확인 + +### 1-4. 숫자 포맷 locale 명시 +- **파일**: `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` (line 65-68) +- **문제**: `toLocaleString(undefined, ...)` → Windows 지역 설정에 따라 포맷 달라짐 +- **영향 범위**: 해당 페이지 숫자 표시 (다른 곳에도 동일 패턴 있는지 추가 검색 필요) +- **수정 방향**: + - `undefined` → `'ko-KR'` 명시적 locale 지정 + - 프로젝트 전체에서 동일 패턴 일괄 검색 후 수정 +- [ ] `toLocaleString(undefined` 패턴 전체 검색 +- [ ] locale을 `'ko-KR'`로 일괄 변경 + +--- + +## Phase 2: MINOR — 안정성/효율성 개선 + +### 2-1. number input 스피너 버튼 숨김 +- **파일**: 품질관리 문서 등 `input[type="number"]` 사용처 +- **문제**: Windows에서 ↑↓ 스피너 버튼 표시 → 터치스크린에서 실수 클릭 +- **수정 방향**: 전역 CSS로 스피너 숨김 처리 +- [ ] globals.css에 `input[type="number"]` 스피너 숨김 CSS 추가 + +### 2-2. 클립보드 API 에러 핸들링 +- **파일**: `src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx` (line 44-48) +- **문제**: `navigator.clipboard.writeText()` 가 Windows 보안 정책으로 실패 가능 +- **수정 방향**: try-catch + `document.execCommand('copy')` fallback +- [ ] 클립보드 사용 코드 에러 핸들링 추가 + +### 2-3. 출퇴근 시계 갱신 주기 최적화 +- **파일**: `src/app/[locale]/(protected)/hr/attendance/page.tsx` (line ~170-180) +- **문제**: `setInterval(1000)` 매초 갱신 → 공장 PC CPU 부하 +- **수정 방향**: 날짜만 표시하므로 60초 간격으로 변경 +- [ ] setInterval 주기 1초 → 60초로 변경 + +--- + +## 테스트 체크리스트 + +모든 수정 후 Windows 환경에서 확인: + +- [ ] DatePicker: Dialog 안에서 날짜 선택 → 값 정상 입력 +- [ ] DatePicker: 이전/다음달 날짜 클릭 → 팝업 유지, 월 이동 +- [ ] TimePicker: Dialog 안에서 시간 선택 → 정상 동작 +- [ ] 스크롤: 메인 레이아웃 + 테이블 스크롤 부드러움 확인 +- [ ] 폰트: 텍스트 두께가 macOS와 비슷한 수준인지 확인 +- [ ] 숫자 포맷: 천단위 구분자, 소수점 정상 표시 +- [ ] number input: 스피너 버튼 안 보이는지 확인 + +--- + +## 참고 + +- Windows 공장 PC 사양: 보통 중저사양 (Intel i3-i5, 8GB RAM, 내장 GPU) +- 브라우저: Chrome 또는 Edge (Chromium 기반) +- 터치스크린 사용 가능성 있음 diff --git a/src/app/[locale]/(protected)/accounting/gift-certificates/page.tsx b/src/app/[locale]/(protected)/accounting/gift-certificates/page.tsx index b36faedd..6211c99c 100644 --- a/src/app/[locale]/(protected)/accounting/gift-certificates/page.tsx +++ b/src/app/[locale]/(protected)/accounting/gift-certificates/page.tsx @@ -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 ; } + if (mode === 'view' && id) { + if (isLoading) return ; + return ( + + ); + } + if (mode === 'edit' && id) { if (isLoading) return ; return ( diff --git a/src/app/[locale]/(protected)/board/[boardCode]/page.tsx b/src/app/[locale]/(protected)/board/[boardCode]/page.tsx index b8d049ea..bf9489ab 100644 --- a/src/app/[locale]/(protected)/board/[boardCode]/page.tsx +++ b/src/app/[locale]/(protected)/board/[boardCode]/page.tsx @@ -311,7 +311,7 @@ function BoardListContent({ boardCode }: { boardCode: string }) {
- #{globalIndex} + {globalIndex} {item.isNotice && ( 공지 )} diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx index ab47180d..fa0a3698 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/page.tsx @@ -318,7 +318,7 @@ function DynamicBoardListContent({ boardCode }: { boardCode: string }) {
- #{globalIndex} + {globalIndex} {item.isNotice && ( 공지 )} diff --git a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx index c39022d6..b7c008fc 100644 --- a/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx +++ b/src/app/[locale]/(protected)/sales/client-management-sales-admin/page.tsx @@ -502,7 +502,7 @@ export default function CustomerAccountManagementPage() { variant="outline" className="bg-gray-100 text-gray-700 font-mono text-xs" > - #{globalIndex} + {globalIndex} {customer.code} diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx index 4fd0d94f..49b6d51e 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx @@ -267,8 +267,8 @@ export default function OrderEditPage() { return (
{/* 상태 뱃지 */} -
- +
+ {form.lotNumber} {getOrderStatusBadge(form.status)} @@ -283,7 +283,7 @@ export default function OrderEditPage() { -
+

{form.lotNumber}

@@ -298,7 +298,7 @@ export default function OrderEditPage() {
-

{form.client}

+

{form.client}

diff --git a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx index 1b183728..2a470ab4 100644 --- a/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx +++ b/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx @@ -527,7 +527,7 @@ export default function OrderDetailPage() { 기본 정보 -
+
@@ -555,7 +555,7 @@ export default function OrderDetailPage() { -
+

주소

{order.address || "-"}

@@ -578,10 +578,10 @@ export default function OrderDetailPage() { {/* 제품내용 (아코디언) */} -
- 제품내용 +
+ 제품내용 {((order.nodes && order.nodes.length > 0) || (order.products && order.products.length > 0)) && ( -
+
{/* 부품 목록 (확장 시 표시) */} @@ -701,27 +699,25 @@ export default function OrderDetailPage() { {/* 부품 목록 (확장 시 표시) */} @@ -1206,37 +1202,37 @@ export default function OrderDetailPage() { {/* 생산지시 되돌리기 다이얼로그 */} - - + + 생산지시 되돌리기 -
+
{/* 수주 정보 박스 */} -
-
- 수주번호 - {order.lotNumber} +
+
+ 수주번호 + {order.lotNumber}
-
- 발주처 - {order.client} +
+ 발주처 + {order.client}
-
- 현장명 - {order.siteName} +
+ 현장명 + {order.siteName}
-
- 현재 상태 +
+ 현재 상태 {getOrderStatusBadge(order.status)}
{/* 경고 메시지 */} -
+

⚠️ 되돌리기 시 삭제되는 데이터

  • • 이 수주에 연결된 모든 작업지시가 삭제됩니다
  • @@ -1253,38 +1249,40 @@ export default function OrderDetailPage() { placeholder="되돌리기 사유를 입력해주세요 (선택)" value={revertReason} onChange={(e) => setRevertReason(e.target.value)} - rows={3} + rows={2} />
{/* 확인 안내 */} -
+

이 작업은 되돌릴 수 없습니다. 정말로 생산지시를 되돌리시겠습니까?

- + -
+
{isDev && ( )} )}
diff --git a/src/components/accounting/BankTransactionInquiry/TransactionFormModal.tsx b/src/components/accounting/BankTransactionInquiry/TransactionFormModal.tsx index e0011f3e..054d603f 100644 --- a/src/components/accounting/BankTransactionInquiry/TransactionFormModal.tsx +++ b/src/components/accounting/BankTransactionInquiry/TransactionFormModal.tsx @@ -281,7 +281,7 @@ export function TransactionFormModal({
{/* 거래일 * / 거래시간 */} -
+
{/* 금액 * / 잔액 (자동계산) */} -
+
{/* 하단 버튼 */} -
- {/* 좌측: 원본으로 복원 (③) - 수정 모드에서만 */} -
- {mode === 'edit' && ( - - )} -
- - {/* 우측: 삭제 + 수정/등록 */} +
+ {/* 삭제 + 수정/등록 (50:50) */}
{mode === 'edit' && (
+ + {/* 원본으로 복원 (100%) - 수정 모드에서만 */} + {mode === 'edit' && ( + + )}
diff --git a/src/components/accounting/BillManagement/BillDetail.tsx b/src/components/accounting/BillManagement/BillDetail.tsx index 16db08ff..1021d73e 100644 --- a/src/components/accounting/BillManagement/BillDetail.tsx +++ b/src/components/accounting/BillManagement/BillDetail.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { Plus, X } from 'lucide-react'; +import { Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; @@ -428,13 +428,14 @@ export function BillDetail({ billId, mode }: BillDetailProps) { )} +
No - 일자 - 금액 - 비고 + 일자 + 금액 + 비고 {!isViewMode && 삭제} @@ -480,7 +481,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) { className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => handleRemoveInstallment(inst.id)} > - + )} @@ -489,6 +490,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) { )}
+
diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx index d3070897..9d8e1290 100644 --- a/src/components/accounting/BillManagement/BillManagementClient.tsx +++ b/src/components/accounting/BillManagement/BillManagementClient.tsx @@ -247,7 +247,7 @@ export function BillManagementClient({ isSelected={handlers.isSelected} onToggleSelection={handlers.onToggle} infoGrid={ -
+
@@ -387,35 +387,39 @@ export function BillManagementClient({ icon: Plus, }, - // 헤더 액션: 상태 선택 + 저장 + 수취/발행 라디오 + // 헤더 액션: 수취/발행 라디오 + 상태 선택 + 저장 + // 모바일: 라디오/상태필터는 숨기고 저장만 표시 (filterConfig 바텀시트와 중복 방지) + // 데스크톱: 모두 표시 headerActions: () => ( -
- { setBillTypeFilter(value); loadData(1); }} - className="flex items-center gap-3" - > -
- - -
-
- - -
-
- +
+
+ { setBillTypeFilter(value); loadData(1); }} + className="flex items-center gap-3" + > +
+ + +
+
+ + +
+
+ +
{/* 사용일 + 사용시간 (공통 DatePicker/TimePicker) */} -
+
{/* 승인번호 + 승인유형 */} -
+
{/* 공급가액 + 세액 */} -
+
{/* 가맹점명 + 사업자번호 */} -
+
{/* 공제여부 + 계정과목 (Select - FormField 예외) */} -
+
{/* 증빙/판매자상호 + 내역 */} -
+
{/* 합계 금액 */} -
+
합계 금액 (공급가액 + 세액) {formatNumber(totalAmount)}원
diff --git a/src/components/accounting/DailyReport/actions.ts b/src/components/accounting/DailyReport/actions.ts index 732a2271..c5d442ad 100644 --- a/src/components/accounting/DailyReport/actions.ts +++ b/src/components/accounting/DailyReport/actions.ts @@ -104,42 +104,3 @@ export async function getDailyReportSummary(params?: { }); } -// ===== 일일 보고서 엑셀 다운로드 ===== -export async function exportDailyReportExcel(params?: { - date?: string; -}): Promise<{ - success: boolean; - data?: Blob; - filename?: string; - error?: string; -}> { - try { - const cookieStore = await cookies(); - const token = cookieStore.get('access_token')?.value; - - const headers: HeadersInit = { - 'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'Authorization': token ? `Bearer ${token}` : '', - 'X-API-KEY': process.env.API_KEY || '', - }; - - const response = await fetch( - buildApiUrl('/api/v1/daily-report/export', { date: params?.date }), - { method: 'GET', headers } - ); - - if (!response.ok) { - return { success: false, error: `API 오류: ${response.status}` }; - } - - const blob = await response.blob(); - const contentDisposition = response.headers.get('Content-Disposition'); - const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${params?.date || 'today'}.xlsx`; - - return { success: true, data: blob, filename }; - } catch (error) { - if (isNextRedirectError(error)) throw error; - console.error('[DailyReportActions] exportDailyReportExcel error:', error); - return { success: false, error: '서버 오류가 발생했습니다.' }; - } -} \ No newline at end of file diff --git a/src/components/accounting/DailyReport/index.tsx b/src/components/accounting/DailyReport/index.tsx index d557e4c3..b1bd44cb 100644 --- a/src/components/accounting/DailyReport/index.tsx +++ b/src/components/accounting/DailyReport/index.tsx @@ -22,7 +22,7 @@ import { formatNumber as formatAmount } from '@/lib/utils/amount'; import { Badge } from '@/components/ui/badge'; import type { NoteReceivableItem, DailyAccountItem } from './types'; import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types'; -import { getNoteReceivables, getDailyAccounts, getDailyReportSummary, exportDailyReportExcel } from './actions'; +import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { usePermission } from '@/hooks/usePermission'; @@ -156,22 +156,32 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts } }, [selectedDate]); - // ===== 엑셀 다운로드 ===== + // ===== 엑셀 다운로드 (프록시 API 직접 호출) ===== const handleExcelDownload = useCallback(async () => { - const result = await exportDailyReportExcel({ date: selectedDate }); + try { + const url = `/api/proxy/daily-report/export?date=${selectedDate}`; + const response = await fetch(url); - if (result.success && result.data) { - const url = URL.createObjectURL(result.data); + if (!response.ok) { + toast.error(`엑셀 다운로드에 실패했습니다. (${response.status})`); + return; + } + + const blob = await response.blob(); + const contentDisposition = response.headers.get('Content-Disposition'); + const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${selectedDate}.xlsx`; + + const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); - a.href = url; - a.download = result.filename || `일일일보_${selectedDate}.xlsx`; + a.href = blobUrl; + a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); - URL.revokeObjectURL(url); + URL.revokeObjectURL(blobUrl); toast.success('엑셀 파일이 다운로드되었습니다.'); - } else { - toast.error(result.error || '엑셀 다운로드에 실패했습니다.'); + } catch { + toast.error('엑셀 다운로드 중 오류가 발생했습니다.'); } }, [selectedDate]); @@ -187,18 +197,17 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts {/* 헤더 액션 (날짜 선택, 버튼 등) */} -
-
-
- - 조회 일자 - -
+
+
+ + 조회 일자 +
{canExport && ( - )}
@@ -232,13 +242,14 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts

어음 및 외상매출채권현황

+
- 내용 - 현재 잔액 - 발행일 - 만기일 + 내용 + 현재 잔액 + 발행일 + 만기일 @@ -260,10 +271,10 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts ) : ( noteReceivables.map((item) => ( - {item.content} - {formatAmount(item.currentBalance)} - {item.issueDate} - {item.dueDate} + {item.content} + {formatAmount(item.currentBalance)} + {item.issueDate} + {item.dueDate} )) )} @@ -272,13 +283,14 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts 합계 - {formatAmount(noteReceivableTotal)} + {formatAmount(noteReceivableTotal)} )}
+
@@ -292,15 +304,16 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
+
- 구분 - 상태 - 전월 이월 - 수입 - 지출 - 잔액 + 구분 + 상태 + 전월 이월 + 수입 + 지출 + 잔액 @@ -326,16 +339,16 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts .filter(item => item.currency === 'KRW') .map((item) => ( - {item.category} - + {item.category} + {MATCH_STATUS_LABELS[item.matchStatus]} - {formatAmount(item.carryover)} - {formatAmount(item.income)} - {formatAmount(item.expense)} - {formatAmount(item.balance)} + {formatAmount(item.carryover)} + {formatAmount(item.income)} + {formatAmount(item.expense)} + {formatAmount(item.balance)} ))} @@ -345,25 +358,26 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts {/* 외화원 (USD) 합계 */} - 외화원 (USD) 합계 + 외화원 (USD) 합계 - ${formatAmount(accountTotals.usd.carryover)} - ${formatAmount(accountTotals.usd.income)} - ${formatAmount(accountTotals.usd.expense)} - ${formatAmount(accountTotals.usd.balance)} + ${formatAmount(accountTotals.usd.carryover)} + ${formatAmount(accountTotals.usd.income)} + ${formatAmount(accountTotals.usd.expense)} + ${formatAmount(accountTotals.usd.balance)} {/* 현금성 자산 합계 */} - 현금성 자산 합계 + 현금성 자산 합계 - {formatAmount(cashAssetTotal.carryover)} - {formatAmount(cashAssetTotal.income)} - {formatAmount(cashAssetTotal.expense)} - {formatAmount(cashAssetTotal.balance)} + {formatAmount(cashAssetTotal.carryover)} + {formatAmount(cashAssetTotal.income)} + {formatAmount(cashAssetTotal.expense)} + {formatAmount(cashAssetTotal.balance)} )}
+
diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index b75f53f3..b1cd6cab 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -765,11 +765,18 @@ export function ExpectedExpenseManagement({ isSelected={isSelected} onToggleSelection={onToggle} infoGrid={ -
- - - - +
+ {[ + { label: '예상 지급일', value: item.expectedPaymentDate }, + { label: '지출금액', value: `${formatNumber(item.amount)}원` }, + { label: '거래처', value: item.vendorName }, + { label: '계좌', value: item.bankAccount || '-' }, + ].map((field) => ( +
+ {field.label} + {field.value} +
+ ))}
} /> @@ -1080,13 +1087,13 @@ export function ExpectedExpenseManagement({ {/* 등록/수정 폼 다이얼로그 */} - + {editingItem ? '미지급비용 수정' : '미지급비용 등록'} -
+
{/* 예상 지급일 */} -
+
{/* 거래처 / 금액 */} -
+
setFormData(prev => ({ ...prev, paymentStatus: value as PaymentStatus }))} > - + diff --git a/src/components/accounting/GeneralJournalEntry/AccountSubjectSettingModal.tsx b/src/components/accounting/GeneralJournalEntry/AccountSubjectSettingModal.tsx index 31cfcb88..74ead516 100644 --- a/src/components/accounting/GeneralJournalEntry/AccountSubjectSettingModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/AccountSubjectSettingModal.tsx @@ -205,74 +205,78 @@ export function AccountSubjectSettingModal({ {/* 추가 영역 */} -
- +
+ + +
+
+
+ + +
+ +
+
+ + {/* 검색/필터 영역 */} +
+ setSearch(e.target.value)} + className="w-full sm:max-w-[250px] h-9 text-sm" /> - -
- - + - {ACCOUNT_CATEGORY_OPTIONS.map((opt) => ( + {ACCOUNT_CATEGORY_FILTER_OPTIONS.map((opt) => ( {opt.label} ))} + + {filteredSubjects.length}개 +
- -
- - {/* 검색/필터 영역 */} -
- setSearch(e.target.value)} - className="max-w-[250px] h-9 text-sm" - /> - - - {filteredSubjects.length}개 -
{/* 테이블 */} diff --git a/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx b/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx index fb9b6755..1785dbb4 100644 --- a/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/JournalEditModal.tsx @@ -278,7 +278,7 @@ export function JournalEditModal({ {/* 거래 정보 (읽기전용) */} -
+
{record.date}
@@ -323,13 +323,13 @@ export function JournalEditModal({
{/* 분개 테이블 */} -
+
{isLoading ? (
로딩 중...
) : ( - +
구분 diff --git a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx index af3cfa46..663a2944 100644 --- a/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx +++ b/src/components/accounting/GeneralJournalEntry/ManualJournalEntryModal.tsx @@ -202,7 +202,7 @@ export function ManualJournalEntryModal({ {/* 거래 정보 */} -
+
{/* 분개 테이블 */} -
-
+
+
구분 diff --git a/src/components/accounting/GeneralJournalEntry/index.tsx b/src/components/accounting/GeneralJournalEntry/index.tsx index 074e55f6..0a123dff 100644 --- a/src/components/accounting/GeneralJournalEntry/index.tsx +++ b/src/components/accounting/GeneralJournalEntry/index.tsx @@ -316,8 +316,6 @@ export function GeneralJournalEntry() { subtitle={item.date} badge={JOURNAL_DIVISION_LABELS[item.division] || item.division} badgeVariant="outline" - isSelected={false} - onToggle={() => {}} onClick={() => setJournalEditTarget(item)} details={[ { label: '입금', value: `${formatNumber(item.depositAmount || 0)}원` }, diff --git a/src/components/accounting/GiftCertificateManagement/GiftCertificateDetail.tsx b/src/components/accounting/GiftCertificateManagement/GiftCertificateDetail.tsx index 57212403..21895355 100644 --- a/src/components/accounting/GiftCertificateManagement/GiftCertificateDetail.tsx +++ b/src/components/accounting/GiftCertificateManagement/GiftCertificateDetail.tsx @@ -3,9 +3,9 @@ import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; import { FormField } from '@/components/molecules/FormField'; import { PageLayout, PageHeader } from '@/components/organisms'; +import { DetailActions } from '@/components/templates/IntegratedDetailTemplate/components/DetailActions'; import { Gift, AlertCircle } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -23,7 +23,7 @@ import { import type { GiftCertificateFormData } from './types'; interface GiftCertificateDetailProps { - mode: 'new' | 'edit'; + mode: 'new' | 'edit' | 'view'; initialData?: GiftCertificateFormData; id?: string; } @@ -40,6 +40,8 @@ export function GiftCertificateDetail({ const [isSubmitting, setIsSubmitting] = useState(false); const isNew = mode === 'new'; + const isView = mode === 'view'; + const isEditable = !isView; const showUsageInfo = formData.faceValue >= FACE_VALUE_THRESHOLD; const handleChange = useCallback( @@ -110,11 +112,13 @@ export function GiftCertificateDetail({ return ( @@ -135,21 +139,23 @@ export function GiftCertificateDetail({ /> handleChange('name', v)} placeholder="상품권명을 입력하세요" + disabled={!isEditable} />
handleChange('faceValue', v ?? 0)} placeholder="0" + disabled={!isEditable} />
handleChange('purchaseDate', v)} + disabled={!isEditable} /> handleChange('purchasePurpose', v)} options={PURCHASE_PURPOSE_OPTIONS} + disabled={!isEditable} />
@@ -186,6 +195,7 @@ export function GiftCertificateDetail({ value={formData.entertainmentExpense} onChange={(v) => handleChange('entertainmentExpense', v)} options={ENTERTAINMENT_EXPENSE_OPTIONS} + disabled={!isEditable} /> handleChange('status', v)} options={statusDetailOptions} + disabled={!isEditable} /> @@ -223,13 +234,15 @@ export function GiftCertificateDetail({ type="date" value={formData.usedDate} onChange={(v) => handleChange('usedDate', v)} + disabled={!isEditable} /> handleChange('recipientName', v)} placeholder="수령인 이름" - required={showUsageInfo} + required={showUsageInfo && isEditable} + disabled={!isEditable} /> @@ -239,12 +252,14 @@ export function GiftCertificateDetail({ value={formData.recipientOrganization} onChange={(v) => handleChange('recipientOrganization', v)} placeholder="회사명" + disabled={!isEditable} /> handleChange('usageDescription', v)} placeholder="내용" + disabled={!isEditable} /> @@ -255,32 +270,22 @@ export function GiftCertificateDetail({ onChange={(v) => handleChange('memo', v)} placeholder="비고 사항을 입력하세요" rows={3} + disabled={!isEditable} /> - {/* 하단 버튼 */} -
- {!isNew && ( - - )} - - -
+ {/* 하단 버튼 (DetailActions sticky 패턴) */} + router.push('/ko/accounting/gift-certificates')} + onCancel={() => router.push('/ko/accounting/gift-certificates')} + onEdit={() => router.push(`/ko/accounting/gift-certificates?mode=edit&id=${id}`)} + onDelete={!isNew ? handleDelete : undefined} + onSubmit={handleSubmit} + />
); } diff --git a/src/components/accounting/GiftCertificateManagement/actions.ts b/src/components/accounting/GiftCertificateManagement/actions.ts index 3e7b96d6..f2304e82 100644 --- a/src/components/accounting/GiftCertificateManagement/actions.ts +++ b/src/components/accounting/GiftCertificateManagement/actions.ts @@ -48,7 +48,25 @@ export async function getGiftCertificateById( // transform: transformDetailApiToFrontend, // errorMessage: '상품권 조회에 실패했습니다.', // }); - return { success: false, error: '상품권을 찾을 수 없습니다.' }; + return { + success: true, + data: { + serialNumber: 'GC-2026-001', + name: '신세계 상품권', + faceValue: 500000, + vendorId: '', + vendorName: '신세계백화점', + purchaseDate: '2026-02-10', + purchasePurpose: 'entertainment', + entertainmentExpense: 'applicable', + status: 'used', + usedDate: '2026-02-20', + recipientName: '홍길동', + recipientOrganization: '(주)테크솔루션', + usageDescription: '거래처 접대용', + memo: '2월 접대비 처리 완료', + }, + }; } // ===== 상품권 등록 (Mock) ===== diff --git a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx index 07b9efea..7966b22e 100644 --- a/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx +++ b/src/components/accounting/PurchaseManagement/PurchaseDetail.tsx @@ -21,8 +21,9 @@ import { FileText, Eye } from 'lucide-react'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { LineItemsTable, useLineItems } from '@/components/organisms/LineItemsTable'; import { purchaseConfig } from './purchaseConfig'; -import { DocumentDetailModal } from '@/components/approval/DocumentDetail'; -import type { ProposalDocumentData, ExpenseReportDocumentData } from '@/components/approval/DocumentDetail/types'; +import { DocumentDetailModalV2 as DocumentDetailModal } from '@/components/approval/DocumentDetail'; +import type { ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types'; +import { getApprovalById } from '@/components/approval/DocumentCreate/actions'; import type { PurchaseRecord, PurchaseItem, PurchaseType } from './types'; import { PURCHASE_TYPE_LABELS } from './types'; import { @@ -34,7 +35,6 @@ import { import { getClients } from '../VendorManagement/actions'; import { toast } from 'sonner'; import { formatNumber as formatAmount } from '@/lib/utils/amount'; -import { formatDate } from '@/lib/utils/date'; interface PurchaseDetailProps { purchaseId: string; @@ -83,8 +83,11 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { const [withdrawalAccount, setWithdrawalAccount] = useState(undefined); const [createdAt, setCreatedAt] = useState(''); - // ===== 다이얼로그 상태 ===== + // ===== 문서 열람 모달 상태 ===== const [documentModalOpen, setDocumentModalOpen] = useState(false); + const [modalData, setModalData] = useState(null); + const [isModalLoading, setIsModalLoading] = useState(false); + const [approvalId, setApprovalId] = useState(undefined); // ===== 품목 관리 (공통 훅) ===== const { handleItemChange, handleAddItem, handleRemoveItem, totals } = useLineItems({ @@ -129,6 +132,7 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { setSourceDocument(data.sourceDocument); setWithdrawalAccount(data.withdrawalAccount); setCreatedAt(data.createdAt); + setApprovalId(data.approvalId); } } else if (isNewMode) { setPurchaseNo('(자동생성)'); @@ -139,6 +143,89 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { loadInitialData(); }, [purchaseId, mode, isNewMode]); + // ===== 문서 열람 핸들러 (API 연동) ===== + const handleOpenDocument = useCallback(async () => { + if (!approvalId) { + toast.error('연결된 결재 문서가 없습니다.'); + return; + } + + setIsModalLoading(true); + setDocumentModalOpen(true); + + try { + const result = await getApprovalById(parseInt(approvalId)); + if (result.success && result.data) { + const formData = result.data; + const docType = sourceDocument?.type === 'expense_report' ? 'expenseReport' : 'proposal'; + + // 기안자 정보 + const drafter = { + id: 'drafter-1', + name: formData.basicInfo.drafter, + position: formData.basicInfo.drafterPosition || '', + department: formData.basicInfo.drafterDepartment || '', + status: 'approved' as const, + }; + + // 결재자 정보 + const approvers = formData.approvalLine.map((person) => ({ + id: person.id, + name: person.name, + position: person.position, + department: person.department, + status: 'approved' as const, + })); + + if (docType === 'expenseReport') { + setModalData({ + documentNo: formData.basicInfo.documentNo, + createdAt: formData.basicInfo.draftDate, + requestDate: formData.expenseReportData?.requestDate || '', + paymentDate: formData.expenseReportData?.paymentDate || '', + items: formData.expenseReportData?.items.map((item, index) => ({ + id: item.id, + no: index + 1, + description: item.description, + amount: item.amount, + note: item.note, + })) || [], + cardInfo: formData.expenseReportData?.cardId || '-', + totalAmount: formData.expenseReportData?.totalAmount || 0, + attachments: formData.expenseReportData?.uploadedFiles?.map(f => f.name) || [], + approvers, + drafter, + } as ExpenseReportDocumentData); + } else { + const uploadedFileUrls = (formData.proposalData?.uploadedFiles || []).map(f => + `/api/proxy/files/${f.id}/download` + ); + setModalData({ + documentNo: formData.basicInfo.documentNo, + createdAt: formData.basicInfo.draftDate, + vendor: formData.proposalData?.vendor || '-', + vendorPaymentDate: formData.proposalData?.vendorPaymentDate || '', + title: formData.proposalData?.title || '', + description: formData.proposalData?.description || '-', + reason: formData.proposalData?.reason || '-', + estimatedCost: formData.proposalData?.estimatedCost || 0, + attachments: uploadedFileUrls, + approvers, + drafter, + } as ProposalDocumentData); + } + } else { + toast.error(result.error || '문서 조회에 실패했습니다.'); + setDocumentModalOpen(false); + } + } catch { + toast.error('문서 조회 중 오류가 발생했습니다.'); + setDocumentModalOpen(false); + } finally { + setIsModalLoading(false); + } + }, [approvalId, sourceDocument]); + // ===== 핸들러 ===== const handleVendorChange = useCallback((clientId: string) => { const client = clients.find(c => c.id === clientId); @@ -224,16 +311,18 @@ export function PurchaseDetail({ purchaseId, mode }: PurchaseDetailProps) { {sourceDocument ? ( <> {/* 문서 타입 및 열람 버튼 */} -
- - {sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} - - 연결된 문서가 있습니다 +
+
+ + {sourceDocument.type === 'proposal' ? '품의서' : '지출결의서'} + + 연결된 문서가 있습니다 +
{/* 거래처/연체 - 왼쪽 고정 */} - + 거래처 / 연체 - {/* 구분 - 왼쪽 고정 (거래처 옆) */} - + {/* 구분 - sm 이상에서만 왼쪽 고정 */} + 구분 {/* 동적 월 레이블 - 스크롤 영역 */} @@ -465,8 +467,8 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma {month} ))} - {/* 합계 - 오른쪽 고정 */} - + {/* 합계 - sm 이상에서만 오른쪽 고정 */} + 합계 @@ -514,7 +516,7 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma > {/* 거래처명 - 왼쪽 고정 (매 행마다 개별 셀, 첫 행만 내용 표시) */} {catIndex === 0 && (
@@ -533,8 +535,8 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma )} - {/* 구분 - 왼쪽 고정 */} - + {/* 구분 - sm 이상에서만 왼쪽 고정 */} + {CATEGORY_LABELS[category]} @@ -548,8 +550,8 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma ))} - {/* 합계 - 오른쪽 고정 */} - + {/* 합계 - sm 이상에서만 오른쪽 고정 */} + {formatAmount(categoryData.amounts.total)} @@ -561,10 +563,10 @@ export function ReceivablesStatus({ highlightVendorId, initialData, initialSumma rows.push( {/* 거래처명 셀 (빈 셀 - 시각적 병합 유지) */} - + {/* 구분: 메모 + 접기/펼치기 버튼 */} - +
메모
+ + {/* 모바일 카드 뷰 - xl 미만에서만 표시 */} +
+ {isLoading ? ( + + +
+ + 데이터를 불러오는 중... +
+
+
+ ) : sortedData.length === 0 ? ( + + + 검색 결과가 없습니다. + + + ) : ( + <> + {sortedData.map((vendor) => { + const salesCat = vendor.categories.find(c => c.category === 'sales'); + const depositCat = vendor.categories.find(c => c.category === 'deposit'); + const billCat = vendor.categories.find(c => c.category === 'bill'); + const receivableCat = vendor.categories.find(c => c.category === 'receivable'); + const isHighlighted = highlightVendorId === vendor.id; + + return ( + 연체 + ) : null + } + subtitle={`미수금 합계: ${formatNumber(receivableCat?.amounts.total || 0)}원`} + collapsible + defaultExpanded={false} + infoGrid={ +
+ {[ + { label: '매출', value: salesCat?.amounts.total || 0 }, + { label: '입금', value: depositCat?.amounts.total || 0 }, + { label: '어음', value: billCat?.amounts.total || 0 }, + { label: '미수금', value: receivableCat?.amounts.total || 0, bold: true }, + ].map((item) => ( +
+ {item.label} + + {formatNumber(item.value)}원 + +
+ ))} +
+ } + bottomContent={ +
e.stopPropagation()}> +
+ handleOverdueToggle(vendor.id, checked)} + className="data-[state=checked]:bg-red-500" + /> + 연체 설정 +
+