diff --git a/claudedocs/[REPORT-2026-02-23] E2E-remaining-bugs-handoff.md b/claudedocs/[REPORT-2026-02-23] E2E-remaining-bugs-handoff.md new file mode 100644 index 00000000..d5867b29 --- /dev/null +++ b/claudedocs/[REPORT-2026-02-23] E2E-remaining-bugs-handoff.md @@ -0,0 +1,129 @@ +# E2E 잔여 버그 전달 사항 + +**작성일**: 2026-02-23 +**근거**: `sam-hotfix/e2e/results/hotfix/HOTFIX-REPORT_dev-team_2026-02-20.md` + +--- + +## 프론트엔드 수정 완료 (3건 + 1건) + +| Bug ID | 내용 | 수정 상태 | +|--------|------|:---------:| +| BUG-SORT-001 | 컬럼 정렬 미구현 (14개 페이지) | ✅ 완료 | +| BUG-FILTER-001 | 매출관리 필터 미동작 | ✅ 완료 | +| BUG-REDIRECT-001 | 어음/입금 등록 후 리다이렉트 | ✅ 완료 | +| BUG-BATCH-DELETE-001 (입금) | 삭제 후 빈 페이지 표시 | ✅ 완료 (UniversalListPage 공통 수정) | + +--- + +## QA팀 확인 요청 (1건) + +### BUG-BATCH-DELETE-001 (어음관리) — E2E 테스트 패턴 불일치 + +**현상**: `batch-create-acc-bills` 시나리오에서 VERIFY 단계 FAIL (기대 3건, 실제 0건) + +**원인 분석**: +- E2E 테스트가 `E2E_TEST_어음_{timestamp}` 패턴으로 데이터를 검색 +- 그러나 실제 어음번호는 프론트엔드에서 `E2E_TEST_EB` 접두사로 생성됨 +- **백엔드 API 확인 결과**: `BillService.php:106`에서 `bill_number`를 프론트가 보낸 그대로 저장 (변환 없음) +- `StoreBillRequest.php` 검증: `nullable|string|max:50` — 접두사 제한 없음 + +**결론**: API는 정상. **E2E 테스트 스크립트의 검색 패턴(`E2E_TEST_어음_`)이 실제 생성 데이터 패턴(`E2E_TEST_EB`)과 불일치** + +**요청 사항**: +- E2E 테스트의 어음번호 검색 패턴을 실제 프론트엔드가 생성하는 패턴에 맞게 수정 +- 또는 프론트엔드 어음 등록 폼에서 E2E 테스트 시 사용하는 어음번호 필드값 확인 + +--- + +## 백엔드팀 수정 요청 (1건) + +### BUG-PERF-001 — 품목관리 API 성능 문제 (10초+ 지연) + +**현상**: 생산관리 > 품목관리 (`/api/v1/items`) 테이블 로드 10초 타임아웃 + +**원인 분석** (sam-api 코드 확인): + +#### 병목 1: `getItemsWithInspectionTemplate()` — 전체 테이블 스캔 (5-8초) + +**파일**: `app/Services/ItemService.php` (lines 1024-1060) + +```php +$templates = \DB::table('document_templates') + ->where('tenant_id', $tenantId) + ->where('is_active', true) + ->whereNotNull('linked_item_ids') + ->where(function ($q) use ($categoryCode, $categoryName) { + $q->where('category', $categoryCode) + ->orWhere('category', $categoryName) + ->orWhere('category', 'LIKE', "%{$categoryName}%"); + }) + ->get(['linked_item_ids']); // ← limit 없이 전체 로드 +``` + +- `linked_item_ids`는 JSON 컬럼 → 인덱스 불가 +- 페이지에 20개만 표시하는데 **모든 document_templates 로드** 후 PHP에서 수동 매칭 +- 템플릿 수가 많을수록 지연 증가 + +#### 병목 2: N+1 쿼리 (2-3초) + +**파일**: `app/Services/ItemService.php` (lines 376-390) + +```php +->with(['category:id,name', 'details', 'files']); +``` + +- `details` (hasOne): 아이템당 1쿼리 → 20개 = 20쿼리 +- `files` (hasMany + document_type 필터): 아이템당 1쿼리 → 20개 = 20쿼리 +- 합계: ~40개 추가 쿼리 + +#### 병목 3: 누락 인덱스 + +- `files` 테이블: `document_id` + `document_type` 복합 인덱스 없음 +- `document_templates` 테이블: `linked_item_ids` JSON 인덱스 없음 + +**예상 총 지연**: ~9-11초 (E2E 10초 타임아웃과 일치) + +**수정 제안**: +1. `getItemsWithInspectionTemplate()`에서 필요한 `item_id` 목록만 IN 조건으로 조회하도록 변경 +2. `files`, `item_details` 테이블에 적절한 인덱스 추가 +3. Eager loading 최적화 (`with` 절에 필요한 컬럼만 select) + +--- + +## 백엔드팀 참고 — 신규 리그레션 2건 (API 서버 상태) + +리그레션 리포트(`REGRESSION-REPORT_dev-team_2026-02-20.md`)에서 발견된 신규 이슈. +**3차 테스트에서 PASS → 4차(Pull 후) FAIL로 전환된 건**으로, 서버 상태 확인 필요. + +### BUG-REGRESSION-001: 입금관리 CRUD 실패 (API 500 에러) + +- **시나리오**: `create-delete-acc-deposit` +- **증상**: 다수 API 500 에러 (Welfare, Calendar, TodayIssue API) +- **API 평균 응답**: 3,574ms (통상 84ms의 42배) +- **테이블**: 0건 로드 (데이터 로드 실패) + +### BUG-REGRESSION-002: 자유게시판 CRUD 실패 (API 극심한 지연) + +- **시나리오**: `create-delete-board` +- **증상**: vendorId 옵션 로드 실패, 테이블 로드 5초 타임아웃 +- **API 평균 응답**: 7,752ms (통상 84ms의 92배) +- **에러**: `Failed to load options for vendorId: TypeError: Failed to fetch` + +**공통 추정 원인**: Pull 이후 API 서버 불안정 (500 에러, fetch 실패 다수) + +--- + +## 재검증 명령 + +```bash +# 전체 재검증 +node C:/Users/codeb/sam/e2e/runner/run-all.js + +# 버그별 개별 검증 +node C:/Users/codeb/sam/e2e/runner/run-all.js --filter pagination-sort # BUG-SORT-001 ← 프론트 수정 완료 +node C:/Users/codeb/sam/e2e/runner/run-all.js --filter search-filter # BUG-FILTER-001 ← 프론트 수정 완료 +node C:/Users/codeb/sam/e2e/runner/run-all.js --filter reload-persist # BUG-REDIRECT-001 ← 프론트 수정 완료 +node C:/Users/codeb/sam/e2e/runner/run-all.js --filter batch-create # BUG-BATCH-DELETE-001 ← 프론트 일부 수정 + QA 테스트 패턴 확인 +node C:/Users/codeb/sam/e2e/runner/run-all.js --filter workflow # BUG-PERF-001 ← 백엔드 수정 필요 +``` diff --git a/claudedocs/_index.md b/claudedocs/_index.md index 5b5b183d..12996d70 100644 --- a/claudedocs/_index.md +++ b/claudedocs/_index.md @@ -245,6 +245,41 @@ const today = new Date().toISOString().split('T')[0]; **현재 상태**: `src/` 내 `toISOString().split` 사용 0건 (date.ts 내 구현부 제외) +### 달력/스케줄 공통 리소스 — 작업 전 필수 확인 (2026-02-23) + +달력·일정·날짜 관련 작업 시 아래 공통 리소스를 **반드시 확인**하고 사용할 것. + +**날짜 유틸리티** (`src/lib/utils/date.ts`): +| 함수 | 용도 | +|------|------| +| `getLocalDateString(date)` | Date → `'YYYY-MM-DD'` (KST 안전) | +| `getTodayString()` | 오늘 날짜 문자열 | +| `formatDate(dateStr)` | 표시용 날짜 포맷 (null → `'-'`) | +| `formatDateForInput(dateStr)` | input용 `YYYY-MM-DD` 변환 | +| `formatDateRange(start, end)` | `'시작 ~ 종료'` 포맷 | +| `getDateAfterDays(n)` | N일 후 날짜 | + +**달력 일정 스토어** (`src/stores/useCalendarScheduleStore.ts`): +- 달력관리(CalendarManagement)에서 등록한 공휴일/세무일정/회사일정을 프로젝트 전체에 공유 +- `fetchSchedules(year)` — 연도별 캐시 조회 (API 호출) +- `setSchedulesForYear(year, data)` — 이미 가져온 데이터 직접 설정 +- `invalidateYear(year)` — 캐시 무효화 (등록/수정/삭제 후) +- **현재 상태**: 백엔드 API 미구현 → 호출부 주석 처리 (TODO 검색) + +**달력 이벤트 유틸** (`src/constants/calendarEvents.ts`): +- `isHoliday(date)`, `isTaxDeadline(date)`, `getHolidayName(date)` 등 +- 스토어 우선 → 하드코딩 폴백(2026년) 패턴 +- 새 연도 폴백 데이터 필요 시 이 파일에 `HOLIDAYS_YYYY`, `TAX_DEADLINES_YYYY` 추가 + +**ScheduleCalendar 공통 컴포넌트** (`src/components/common/ScheduleCalendar/`): +- `hideNavigation` prop으로 헤더 ◀ ▶ 숨김 가능 (연간 달력 등 상위 네비게이션 사용 시) +- `availableViews={[]}` 으로 뷰 전환 버튼 숨김 + +**규칙**: +- `Date → string` 변환 시 `getLocalDateString()` 필수 (`toISOString().split('T')[0]` 금지) +- 공휴일/세무일 판별 시 `calendarEvents.ts` 유틸 함수 사용 +- 달력 데이터 공유 시 zustand 스토어 경유 (컴포넌트 간 직접 전달 금지) + ### `useDateRange` 훅 — 날짜 필터 보일러플레이트 제거 (2026-02-19) **현황**: 20+ 리스트 페이지에서 `useState('2025-01-01')` / `useState('2025-12-31')` 하드코딩 diff --git a/src/app/[locale]/(protected)/dashboard_type2/page.tsx b/src/app/[locale]/(protected)/dashboard_type2/page.tsx index 8c5c41d8..4f42966b 100644 --- a/src/app/[locale]/(protected)/dashboard_type2/page.tsx +++ b/src/app/[locale]/(protected)/dashboard_type2/page.tsx @@ -1,6 +1,12 @@ 'use client'; -import { DashboardType2 } from './_components/DashboardType2'; +import dynamic from 'next/dynamic'; +import { DetailPageSkeleton } from '@/components/ui/skeleton'; + +const DashboardType2 = dynamic( + () => import('./_components/DashboardType2').then(mod => ({ default: mod.DashboardType2 })), + { loading: () => } +); /** * Dashboard Type 2 - 탭 기반 대시보드 diff --git a/src/app/[locale]/(protected)/dashboard_type3/page.tsx b/src/app/[locale]/(protected)/dashboard_type3/page.tsx index aaa1a85d..c4fbc736 100644 --- a/src/app/[locale]/(protected)/dashboard_type3/page.tsx +++ b/src/app/[locale]/(protected)/dashboard_type3/page.tsx @@ -1,6 +1,12 @@ 'use client'; -import { DashboardType3 } from './_components/DashboardType3'; +import dynamic from 'next/dynamic'; +import { DetailPageSkeleton } from '@/components/ui/skeleton'; + +const DashboardType3 = dynamic( + () => import('./_components/DashboardType3').then(mod => ({ default: mod.DashboardType3 })), + { loading: () => } +); /** * Dashboard Type 3 - 위젯 보드형 대시보드 diff --git a/src/app/[locale]/(protected)/dashboard_type4/page.tsx b/src/app/[locale]/(protected)/dashboard_type4/page.tsx index 696b06fa..dafe896d 100644 --- a/src/app/[locale]/(protected)/dashboard_type4/page.tsx +++ b/src/app/[locale]/(protected)/dashboard_type4/page.tsx @@ -1,6 +1,12 @@ 'use client'; -import { DashboardType4 } from './_components/DashboardType4'; +import dynamic from 'next/dynamic'; +import { DetailPageSkeleton } from '@/components/ui/skeleton'; + +const DashboardType4 = dynamic( + () => import('./_components/DashboardType4').then(mod => ({ default: mod.DashboardType4 })), + { loading: () => } +); /** * Dashboard Type 4 - KPI 드릴다운형 대시보드 diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx index 96afb0e7..0e599edb 100644 --- a/src/components/accounting/BadDebtCollection/index.tsx +++ b/src/components/accounting/BadDebtCollection/index.tsx @@ -49,12 +49,12 @@ import { deleteBadDebt, toggleBadDebt } from './actions'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'text-center w-[60px]' }, - { key: 'vendorName', label: '거래처', className: 'w-[100px]' }, - { key: 'debtAmount', label: '채권금액', className: 'text-right w-[140px]' }, - { key: 'occurrenceDate', label: '발생일', className: 'text-center w-[110px]' }, - { key: 'overdueDays', label: '연체일수', className: 'text-center w-[100px]' }, - { key: 'managerName', label: '담당자', className: 'w-[100px]' }, - { key: 'status', label: '상태', className: 'text-center w-[100px]' }, + { key: 'vendorName', label: '거래처', className: 'w-[100px]', sortable: true }, + { key: 'debtAmount', label: '채권금액', className: 'text-right w-[140px]', sortable: true }, + { key: 'occurrenceDate', label: '발생일', className: 'text-center w-[110px]', sortable: true }, + { key: 'overdueDays', label: '연체일수', className: 'text-center w-[100px]', sortable: true }, + { key: 'managerName', label: '담당자', className: 'w-[100px]', sortable: true }, + { key: 'status', label: '상태', className: 'text-center w-[100px]', sortable: true }, { key: 'setting', label: '설정', className: 'text-center w-[80px]' }, ]; diff --git a/src/components/accounting/BankTransactionInquiry/index.tsx b/src/components/accounting/BankTransactionInquiry/index.tsx index c41093c9..d58d9c7d 100644 --- a/src/components/accounting/BankTransactionInquiry/index.tsx +++ b/src/components/accounting/BankTransactionInquiry/index.tsx @@ -61,15 +61,15 @@ import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 (체크박스 제외 10개) ===== const tableColumns = [ { key: 'rowNumber', label: 'No.', className: 'text-center w-[50px]' }, - { key: 'transactionDate', label: '거래일시' }, - { key: 'type', label: '구분', className: 'text-center' }, - { key: 'accountInfo', label: '계좌정보' }, - { key: 'note', label: '적요/내용' }, - { key: 'depositAmount', label: '입금', className: 'text-right' }, - { key: 'withdrawalAmount', label: '출금', className: 'text-right' }, - { key: 'balance', label: '잔액', className: 'text-right' }, - { key: 'branch', label: '취급점', className: 'text-center' }, - { key: 'depositorName', label: '상대계좌예금주명' }, + { key: 'transactionDate', label: '거래일시', sortable: true }, + { key: 'type', label: '구분', className: 'text-center', sortable: true }, + { key: 'accountInfo', label: '계좌정보', sortable: true }, + { key: 'note', label: '적요/내용', sortable: true }, + { key: 'depositAmount', label: '입금', className: 'text-right', sortable: true }, + { key: 'withdrawalAmount', label: '출금', className: 'text-right', sortable: true }, + { key: 'balance', label: '잔액', className: 'text-right', sortable: true }, + { key: 'branch', label: '취급점', className: 'text-center', sortable: true }, + { key: 'depositorName', label: '상대계좌예금주명', sortable: true }, ]; // ===== 기본 Summary ===== diff --git a/src/components/accounting/BillManagement/BillDetail.tsx b/src/components/accounting/BillManagement/BillDetail.tsx index 6332a995..16db08ff 100644 --- a/src/components/accounting/BillManagement/BillDetail.tsx +++ b/src/components/accounting/BillManagement/BillDetail.tsx @@ -182,7 +182,7 @@ export function BillDetail({ billId, mode }: BillDetailProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - // ===== 저장 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== + // ===== 저장 핸들러 ===== const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { const validation = validateForm(); if (!validation.valid) { @@ -198,14 +198,20 @@ export function BillDetail({ billId, mode }: BillDetailProps) { }; if (isNewMode) { - return await createBill(billData); + const result = await createBill(billData); + if (result.success) { + toast.success('등록되었습니다.'); + router.push('/ko/accounting/bills'); + return { success: false, error: '' }; // 템플릿의 중복 토스트/리다이렉트 방지 + } + return result; } else { return await updateBill(String(billId), billData); } } finally { setIsSubmitting(false); } - }, [formData, clients, isNewMode, billId, validateForm]); + }, [formData, clients, isNewMode, billId, validateForm, router]); // ===== 삭제 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { diff --git a/src/components/accounting/BillManagement/index.tsx b/src/components/accounting/BillManagement/index.tsx index 38adb76d..393970a9 100644 --- a/src/components/accounting/BillManagement/index.tsx +++ b/src/components/accounting/BillManagement/index.tsx @@ -61,14 +61,14 @@ import { // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'billNumber', label: '어음번호' }, - { key: 'billType', label: '구분', className: 'text-center' }, - { key: 'vendorName', label: '거래처' }, - { key: 'amount', label: '금액', className: 'text-right' }, - { key: 'issueDate', label: '발행일' }, - { key: 'maturityDate', label: '만기일' }, - { key: 'installmentCount', label: '차수', className: 'text-center' }, - { key: 'status', label: '상태', className: 'text-center' }, + { key: 'billNumber', label: '어음번호', sortable: true }, + { key: 'billType', label: '구분', className: 'text-center', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'amount', label: '금액', className: 'text-right', sortable: true }, + { key: 'issueDate', label: '발행일', sortable: true }, + { key: 'maturityDate', label: '만기일', sortable: true }, + { key: 'installmentCount', label: '차수', className: 'text-center', sortable: true }, + { key: 'status', label: '상태', className: 'text-center', sortable: true }, { key: 'actions', label: '작업', className: 'text-center w-[80px]' }, ]; diff --git a/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx b/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx index f134ca6c..6e4dadfb 100644 --- a/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx +++ b/src/components/accounting/DepositManagement/DepositDetailClientV2.tsx @@ -66,7 +66,7 @@ export default function DepositDetailClientV2({ loadDeposit(); }, [depositId, initialMode]); - // ===== 저장/등록 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== + // ===== 저장/등록 핸들러 ===== const handleSubmit = useCallback( async (formData: Record): Promise<{ success: boolean; error?: string }> => { const submitData = depositDetailConfig.transformSubmitData?.(formData) || formData; @@ -81,11 +81,17 @@ export default function DepositDetailClientV2({ ? await createDeposit(submitData as Partial) : await updateDeposit(depositId!, submitData as Partial); + if (result.success && mode === 'create') { + toast.success('등록되었습니다.'); + router.push('/ko/accounting/deposits'); + return { success: false, error: '' }; // 템플릿의 중복 토스트/리다이렉트 방지 + } + return result.success ? { success: true } : { success: false, error: result.error }; }, - [mode, depositId] + [mode, depositId, router] ); // ===== 삭제 핸들러 (결과만 반환, 토스트/리다이렉트는 IntegratedDetailTemplate에 위임) ===== diff --git a/src/components/accounting/ExpectedExpenseManagement/index.tsx b/src/components/accounting/ExpectedExpenseManagement/index.tsx index b579e250..b75f53f3 100644 --- a/src/components/accounting/ExpectedExpenseManagement/index.tsx +++ b/src/components/accounting/ExpectedExpenseManagement/index.tsx @@ -555,12 +555,12 @@ export function ExpectedExpenseManagement({ // ===== 테이블 컬럼 ===== const tableColumns = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, - { key: 'expectedPaymentDate', label: '예상 지급일' }, - { key: 'accountSubject', label: '항목' }, - { key: 'amount', label: '지출금액', className: 'text-right' }, - { key: 'vendorName', label: '거래처' }, - { key: 'bankAccount', label: '계좌' }, - { key: 'approvalStatus', label: '전자결재', className: 'text-center' }, + { key: 'expectedPaymentDate', label: '예상 지급일', sortable: true }, + { key: 'accountSubject', label: '항목', sortable: true }, + { key: 'amount', label: '지출금액', className: 'text-right', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'bankAccount', label: '계좌', sortable: true }, + { key: 'approvalStatus', label: '전자결재', className: 'text-center', sortable: true }, ], []); // ===== 전자결재 상태 Badge 스타일 ===== diff --git a/src/components/accounting/GiftCertificateManagement/index.tsx b/src/components/accounting/GiftCertificateManagement/index.tsx index 1ba7e593..4758f3c4 100644 --- a/src/components/accounting/GiftCertificateManagement/index.tsx +++ b/src/components/accounting/GiftCertificateManagement/index.tsx @@ -53,13 +53,13 @@ import { useDateRange } from '@/hooks'; // ===== 테이블 컬럼 정의 (체크박스/No. 제외) ===== const tableColumns = [ { key: 'rowNumber', label: '번호', className: 'text-center' }, - { key: 'serialNumber', label: '일련번호' }, - { key: 'name', label: '상품권명' }, - { key: 'faceValue', label: '액면가', className: 'text-right' }, - { key: 'purchaseDate', label: '구입일', className: 'text-center' }, - { key: 'usedDate', label: '사용일', className: 'text-center' }, - { key: 'entertainmentExpense', label: '접대비', className: 'text-center' }, - { key: 'status', label: '상태', className: 'text-center' }, + { key: 'serialNumber', label: '일련번호', sortable: true }, + { key: 'name', label: '상품권명', sortable: true }, + { key: 'faceValue', label: '액면가', className: 'text-right', sortable: true }, + { key: 'purchaseDate', label: '구입일', className: 'text-center', sortable: true }, + { key: 'usedDate', label: '사용일', className: 'text-center', sortable: true }, + { key: 'entertainmentExpense', label: '접대비', className: 'text-center', sortable: true }, + { key: 'status', label: '상태', className: 'text-center', sortable: true }, ]; // ===== 기본 Summary ===== diff --git a/src/components/accounting/PurchaseManagement/index.tsx b/src/components/accounting/PurchaseManagement/index.tsx index e1ce96f1..c2665743 100644 --- a/src/components/accounting/PurchaseManagement/index.tsx +++ b/src/components/accounting/PurchaseManagement/index.tsx @@ -68,14 +68,14 @@ import { formatNumber } from '@/lib/utils/amount'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, - { key: 'purchaseNo', label: '매입번호' }, - { key: 'purchaseDate', label: '매입일' }, - { key: 'vendorName', label: '거래처' }, - { key: 'sourceDocument', label: '연결문서', className: 'text-center' }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right' }, - { key: 'vat', label: '부가세', className: 'text-right' }, - { key: 'totalAmount', label: '합계금액', className: 'text-right' }, - { key: 'purchaseType', label: '매입유형', className: 'text-center' }, + { key: 'purchaseNo', label: '매입번호', sortable: true }, + { key: 'purchaseDate', label: '매입일', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'sourceDocument', label: '연결문서', className: 'text-center', sortable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, + { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, + { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, + { key: 'purchaseType', label: '매입유형', className: 'text-center', sortable: true }, { key: 'taxInvoice', label: '세금계산서 수취 확인', className: 'text-center' }, ]; diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index 51bbe243..f05d0d87 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -77,13 +77,13 @@ import { applyFilters, enumFilter } from '@/lib/utils/search'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ { key: 'no', label: '번호', className: 'text-center w-[60px]' }, - { key: 'salesNo', label: '매출번호' }, - { key: 'salesDate', label: '매출일' }, - { key: 'vendorName', label: '거래처' }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right' }, - { key: 'vat', label: '부가세', className: 'text-right' }, - { key: 'totalAmount', label: '합계금액', className: 'text-right' }, - { key: 'salesType', label: '매출유형', className: 'text-center' }, + { key: 'salesNo', label: '매출번호', sortable: true }, + { key: 'salesDate', label: '매출일', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, + { key: 'vat', label: '부가세', className: 'text-right', sortable: true }, + { key: 'totalAmount', label: '합계금액', className: 'text-right', sortable: true }, + { key: 'salesType', label: '매출유형', className: 'text-center', sortable: true }, { key: 'taxInvoice', label: '세금계산서 발행완료', className: 'text-center' }, { key: 'transactionStatement', label: '거래명세서 발행완료', className: 'text-center' }, ]; @@ -320,13 +320,13 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem // 커스텀 필터 함수 (filterConfig 기반 - ULP의 filters state에서 값 전달) // 검색은 searchFilter에서 처리하므로 여기서는 필터만 처리 + // NOTE: salesType 필터는 API에서 매출유형을 제공하지 않아 비활성 (모든 데이터가 'other') customFilterFn: (items, fv) => { if (!items || items.length === 0) return items; const issuanceVal = fv.issuance as string; let result = applyFilters(items, [ enumFilter('vendorName', fv.vendor as string), - enumFilter('salesType', fv.salesType as string), ]); // 발행여부 필터 (특수 로직 - enumFilter로 대체 불가) diff --git a/src/components/accounting/TaxInvoiceManagement/index.tsx b/src/components/accounting/TaxInvoiceManagement/index.tsx index 9be3dd2d..356641c0 100644 --- a/src/components/accounting/TaxInvoiceManagement/index.tsx +++ b/src/components/accounting/TaxInvoiceManagement/index.tsx @@ -12,6 +12,7 @@ */ import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import dynamic from 'next/dynamic'; import { toast } from 'sonner'; import { FileText, @@ -23,6 +24,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { StatCards } from '@/components/organisms/StatCards'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { TableRow, TableCell } from '@/components/ui/table'; @@ -45,8 +47,13 @@ import { getTaxInvoiceSummary, downloadTaxInvoiceExcel, } from './actions'; -import { ManualEntryModal } from './ManualEntryModal'; -import { JournalEntryModal } from './JournalEntryModal'; + +const ManualEntryModal = dynamic( + () => import('./ManualEntryModal').then(mod => ({ default: mod.ManualEntryModal })), +); +const JournalEntryModal = dynamic( + () => import('./JournalEntryModal').then(mod => ({ default: mod.JournalEntryModal })), +); import type { TaxInvoiceMgmtRecord, InvoiceTab, @@ -72,19 +79,19 @@ const QUARTER_BUTTONS = [ // ===== 테이블 컬럼 ===== const tableColumns = [ - { key: 'writeDate', label: '작성일자', className: 'text-center' }, - { key: 'issueDate', label: '발급일자', className: 'text-center' }, - { key: 'vendorName', label: '거래처' }, - { key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center' }, - { key: 'taxType', label: '과세형태', className: 'text-center' }, - { key: 'itemName', label: '품목' }, - { key: 'supplyAmount', label: '공급가액', className: 'text-right' }, - { key: 'taxAmount', label: '세액', className: 'text-right' }, - { key: 'totalAmount', label: '합계', className: 'text-right' }, - { key: 'receiptType', label: '영수청구', className: 'text-center' }, - { key: 'documentType', label: '문서형태', className: 'text-center' }, - { key: 'issueType', label: '발급형태', className: 'text-center' }, - { key: 'status', label: '상태', className: 'text-center' }, + { key: 'writeDate', label: '작성일자', className: 'text-center', sortable: true }, + { key: 'issueDate', label: '발급일자', className: 'text-center', sortable: true }, + { key: 'vendorName', label: '거래처', sortable: true }, + { key: 'vendorBusinessNumber', label: '사업자번호\n(주민번호)', className: 'text-center', sortable: true }, + { key: 'taxType', label: '과세형태', className: 'text-center', sortable: true }, + { key: 'itemName', label: '품목', sortable: true }, + { key: 'supplyAmount', label: '공급가액', className: 'text-right', sortable: true }, + { key: 'taxAmount', label: '세액', className: 'text-right', sortable: true }, + { key: 'totalAmount', label: '합계', className: 'text-right', sortable: true }, + { key: 'receiptType', label: '영수청구', className: 'text-center', sortable: true }, + { key: 'documentType', label: '문서형태', className: 'text-center', sortable: true }, + { key: 'issueType', label: '발급형태', className: 'text-center', sortable: true }, + { key: 'status', label: '상태', className: 'text-center', sortable: true }, { key: 'journal', label: '분개', className: 'text-center w-[80px]' }, ]; @@ -349,18 +356,15 @@ export function TaxInvoiceManagement() { {/* 매출/매입 탭 + 액션 버튼 */}
-
- {TAB_OPTIONS.map((t) => ( - - ))} -
+ handleTabChange(v as InvoiceTab)}> + + {TAB_OPTIONS.map((t) => ( + + {t.label} {t.value === 'sales' ? summary.salesCount : summary.purchaseCount} + + ))} + +