프로젝트관리 — DataGrid 행 id 매핑 누락 + selectedId 가드
증상:
프로젝트관리 진행관리/WBS 그리드의 모든 행이 회색(bg-accent) 으로 표시.
원인 추적 결과 DataGrid 의 isSelected 평가식 `selectedId === row.id` 가
selectedId 도 row.id 도 둘 다 undefined 일 때 `undefined === undefined` = true
가 되어 모든 행이 selected 상태로 잡힘 (memory: feedback_datagrid_id_mapping
함정의 또 다른 발현 — 영업관리는 항상 id 매핑이 있어 우연히 회피).
수정:
- project/progress/page.tsx, project/wbs-template/page.tsx
- setRows 에서 `id: r.objid ?? "...-${i}"` 매핑 추가
- 부모 wrapper 도 영업관리 패턴 `overflow-hidden` + DataGrid 직접 자식으로 통일
- components/common/DataGrid.tsx
- isSelected 가드 — selectedId/row.id 가 nullish 면 무조건 false 처리.
향후 다른 페이지에서 id 매핑 누락 시에도 그리드 색 폭주는 차단.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,7 +126,8 @@ export default function ProjectProgressPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await projectMgmtApi.list(filter);
|
||||
setRows(data);
|
||||
// DataGrid row 키 — objid 없을 수 있어 인덱스 fallback (id 누락 시 모든 행이 selected 로 잡힘)
|
||||
setRows(data.map((r, i) => ({ ...r, id: r.objid ?? `prog-${i}` })) as any);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
} finally { setLoading(false); }
|
||||
@@ -165,7 +166,7 @@ export default function ProjectProgressPage() {
|
||||
}, [rows]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 p-2">
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={fetchList}
|
||||
@@ -256,31 +257,29 @@ export default function ProjectProgressPage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="조건에 맞는 프로젝트가 없습니다."
|
||||
gridId="project-progress-wbslist3"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={progressSummary}
|
||||
onRefresh={fetchList}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "진행관리.xlsx", "진행관리");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
emptyMessage="조건에 맞는 프로젝트가 없습니다."
|
||||
gridId="project-progress-wbslist3"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={progressSummary}
|
||||
onRefresh={fetchList}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "진행관리.xlsx", "진행관리");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
<ProjectInfoDialog open={infoOpen} onOpenChange={setInfoOpen} data={infoData} />
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,8 @@ export default function WbsTemplatePage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await wbsTemplateApi.list(product || undefined);
|
||||
setRows(data);
|
||||
// DataGrid row 키 — objid 없을 수 있어 인덱스 fallback (id 누락 시 모든 행이 selected 로 잡힘)
|
||||
setRows(data.map((r, i) => ({ ...r, id: r.objid ?? `tpl-${i}` })) as any);
|
||||
setCheckedIds([]);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.response?.data?.message ?? e?.message ?? "조회 실패");
|
||||
@@ -126,7 +127,7 @@ export default function WbsTemplatePage() {
|
||||
}, [rows]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2 p-2">
|
||||
<div className="flex h-full flex-col overflow-hidden p-2 gap-2">
|
||||
<PageHeader
|
||||
loading={loading}
|
||||
onSearch={handleSearch}
|
||||
@@ -152,35 +153,33 @@ export default function WbsTemplatePage() {
|
||||
</CompactFilterField>
|
||||
</CompactFilterBar>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="등록된 WBS 템플릿이 없습니다."
|
||||
gridId="project-wbs-template"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={templateSummary}
|
||||
systemColumnKeys={["writer_title", "reg_date_title"]}
|
||||
onRefresh={() => fetchList(filterProduct)}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "WBS_템플릿.xlsx", "WBS_템플릿");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={rows}
|
||||
loading={loading}
|
||||
showRowNumber
|
||||
showCheckbox
|
||||
checkedIds={checkedIds}
|
||||
onCheckedChange={setCheckedIds}
|
||||
emptyMessage="등록된 WBS 템플릿이 없습니다."
|
||||
gridId="project-wbs-template"
|
||||
showColumnSettings
|
||||
paginationStyle="range"
|
||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||
summaryStats={templateSummary}
|
||||
systemColumnKeys={["writer_title", "reg_date_title"]}
|
||||
onRefresh={() => fetchList(filterProduct)}
|
||||
onDownload={() => {
|
||||
if (rows.length === 0) { toast.info("내보낼 데이터가 없습니다."); return; }
|
||||
const exportRows = rows.map((r) => {
|
||||
const out: Record<string, any> = {};
|
||||
GRID_COLUMNS.forEach((col) => { out[col.label] = (r as any)[col.key] ?? ""; });
|
||||
return out;
|
||||
});
|
||||
exportToExcel(exportRows, "WBS_템플릿.xlsx", "WBS_템플릿");
|
||||
}}
|
||||
showChart
|
||||
/>
|
||||
|
||||
{/* 통합 팝업 */}
|
||||
<WbsTemplateDialog
|
||||
|
||||
@@ -896,7 +896,10 @@ export function DataGrid({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : paginatedData.map((row, rowIdx) => {
|
||||
const isSelected = selectedId === row.id || (showCheckbox && checkedIds.includes(row.id));
|
||||
// selectedId 또는 row.id 가 nullish 면 비교 결과를 무조건 false 로 — 둘 다 undefined 일 때 `undefined === undefined` 가 true 가 되어 모든 행이 selected 로 잡히는 함정 차단
|
||||
const isSelected =
|
||||
(selectedId != null && row.id != null && selectedId === row.id) ||
|
||||
(showCheckbox && row.id != null && checkedIds.includes(row.id));
|
||||
// sticky 셀에 alpha 없는 단색 배경 사용 (반투명이면 뒤 셀이 비침).
|
||||
// selected → bg-accent / hover(non-selected) → group-hover로 muted 적용 / 기본 → bg-background
|
||||
const stickyBgClass = isSelected ? "bg-accent" : "bg-background group-hover:bg-muted";
|
||||
|
||||
Reference in New Issue
Block a user