refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차
- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등 - 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리 - 설정 모듈: 계정관리/직급/직책/권한 상세 간소화 - 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리 - UniversalListPage 엑셀 다운로드 및 필터 기능 확장 - 대시보드/게시판/수주 등 날짜 유틸 공통화 적용 - claudedocs 문서 인덱스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
// ===== 테이블 헤더 액션 =====
|
||||
/** 테이블 헤더 우측 추가 액션 (총건, 필터 해제 버튼, 일괄 액션 등)
|
||||
|
||||
Reference in New Issue
Block a user