refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차

- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등
- 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리
- 설정 모듈: 계정관리/직급/직책/권한 상세 간소화
- 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리
- UniversalListPage 엑셀 다운로드 및 필터 기능 확장
- 대시보드/게시판/수주 등 날짜 유틸 공통화 적용
- claudedocs 문서 인덱스 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-20 10:45:47 +09:00
parent 71352923c8
commit f344dc7d00
123 changed files with 877 additions and 789 deletions

View File

@@ -26,6 +26,7 @@ import { ScreenVersionHistory } from "@/components/organisms/ScreenVersionHistor
import { TabChip } from "@/components/atoms/TabChip";
import { MultiSelectCombobox } from "@/components/ui/multi-select-combobox";
import { MobileFilter, FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter";
import { formatNumber } from '@/lib/utils/amount';
/**
* 기본 통합 목록_버젼2
@@ -904,13 +905,13 @@ export function IntegratedListTemplateV2<T = any>({
</Button>
)}
<span className="text-xs text-muted-foreground">
{loadedCount.toLocaleString()} / {totalDataCount.toLocaleString()}
{formatNumber(loadedCount)} / {formatNumber(totalDataCount)}
</span>
</div>
</>
) : (
<div className="text-center py-4 text-sm text-muted-foreground">
({totalDataCount.toLocaleString()})
({formatNumber(totalDataCount)})
</div>
)}
</div>

View File

@@ -120,13 +120,35 @@ export function UniversalListPage<T>({
const filteredData = useMemo(() => {
if (!config.clientSideFiltering) {
// 서버 사이드 모드: searchFilter가 정의되어 있으면 클라이언트 사이드 검색 적용 (백엔드 검색 미지원 대비)
// 서버 사이드 모드
let serverData = rawData;
// searchFilter가 정의되어 있으면 클라이언트 사이드 검색 적용 (백엔드 검색 미지원 대비)
if (debouncedSearchValue && config.searchFilter) {
return rawData.filter((item) =>
serverData = rawData.filter((item) =>
config.searchFilter!(item, debouncedSearchValue)
);
}
return rawData;
// 서버 사이드에서도 컬럼 정렬 지원 (onSortChange 미정의 시 클라이언트 사이드 정렬)
if (sortBy && !config.onSortChange) {
serverData = [...serverData].sort((a, b) => {
const aValue = (a as Record<string, unknown>)[sortBy];
const bValue = (b as Record<string, unknown>)[sortBy];
if (aValue == null && bValue == null) return 0;
if (aValue == null) return sortOrder === 'asc' ? 1 : -1;
if (bValue == null) return sortOrder === 'asc' ? -1 : 1;
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
}
const aStr = String(aValue);
const bStr = String(bValue);
const comparison = aStr.localeCompare(bStr, 'ko');
return sortOrder === 'asc' ? comparison : -comparison;
});
}
return serverData;
}
let filtered = [...rawData];
@@ -194,7 +216,7 @@ export function UniversalListPage<T>({
}
return filtered;
}, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]);
}, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.onSortChange, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]);
// 페이지네이션 (클라이언트 사이드 + 서버 사이드 검색 시)
const paginatedData = useMemo(() => {
@@ -221,7 +243,8 @@ export function UniversalListPage<T>({
: (isServerSearchFiltered ? (Math.ceil(filteredData.length / itemsPerPage) || 1) : serverTotalPages);
// 표시할 데이터
const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : rawData;
// 서버 사이드 모드에서도 filteredData 사용 (클라이언트 사이드 정렬 반영)
const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : filteredData;
// ===== 탭 카운트 계산 (클라이언트 사이드) =====
const computedTabs = useMemo(() => {
@@ -720,21 +743,33 @@ export function UniversalListPage<T>({
// ===== 정렬 핸들러 =====
const handleSort = useCallback((key: string) => {
let newSortBy: string | undefined;
let newSortOrder: 'asc' | 'desc' = 'asc';
if (sortBy === key) {
// 같은 컬럼 클릭: asc → desc → 정렬 해제
if (sortOrder === 'asc') {
setSortOrder('desc');
newSortBy = key;
newSortOrder = 'desc';
} else {
setSortBy(undefined);
setSortOrder('asc');
newSortBy = undefined;
newSortOrder = 'asc';
}
} else {
// 다른 컬럼 클릭: 해당 컬럼으로 asc 정렬
setSortBy(key);
setSortOrder('asc');
newSortBy = key;
newSortOrder = 'asc';
}
setSortBy(newSortBy);
setSortOrder(newSortOrder);
setCurrentPage(1);
}, [sortBy, sortOrder]);
// 서버 사이드 정렬: 부모 컴포넌트에 콜백 전달
if (!config.clientSideFiltering && config.onSortChange) {
config.onSortChange(newSortBy, newSortOrder);
}
}, [sortBy, sortOrder, config.clientSideFiltering, config.onSortChange]);
// ===== 탭 핸들러 =====
const handleTabChange = useCallback((value: string) => {
@@ -922,10 +957,10 @@ export function UniversalListPage<T>({
}
// 테이블 컬럼 (탭별 다른 컬럼 지원)
tableColumns={effectiveColumns}
// 정렬 설정 (클라이언트 사이드 필터링 시에만 활성화)
sortBy={config.clientSideFiltering ? sortBy : undefined}
sortOrder={config.clientSideFiltering ? sortOrder : undefined}
onSort={config.clientSideFiltering ? handleSort : undefined}
// 정렬 설정 (모든 페이지에서 활성화)
sortBy={sortBy}
sortOrder={sortOrder}
onSort={handleSort}
// 커스텀 테이블 헤더 (동적 컬럼용)
renderCustomTableHeader={
config.renderCustomTableHeader

View File

@@ -375,6 +375,8 @@ export interface UniversalListConfig<T> {
customFilterFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[];
/** 커스텀 정렬 함수 */
customSortFn?: (items: T[], filterValues: Record<string, string | string[]>) => T[];
/** 서버 사이드 정렬 콜백 (clientSideFiltering: false일 때 컬럼 헤더 클릭 시 호출) */
onSortChange?: (sortBy: string | undefined, sortOrder: 'asc' | 'desc') => void;
// ===== 테이블 헤더 액션 =====
/** 테이블 헤더 우측 추가 액션 (총건, 필터 해제 버튼, 일괄 액션 등)