프로젝트관리 — 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:
hjjeong
2026-05-14 15:12:34 +09:00
parent 6a1813719a
commit 350ddcd3b8
3 changed files with 60 additions and 59 deletions
@@ -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,7 +257,6 @@ export default function ProjectProgressPage() {
</CompactFilterField>
</CompactFilterBar>
<div className="flex-1 min-h-0">
<DataGrid
columns={columns}
data={rows}
@@ -280,7 +280,6 @@ export default function ProjectProgressPage() {
}}
showChart
/>
</div>
<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,7 +153,6 @@ export default function WbsTemplatePage() {
</CompactFilterField>
</CompactFilterBar>
<div className="flex-1 min-h-0">
<DataGrid
columns={columns}
data={rows}
@@ -180,7 +180,6 @@ export default function WbsTemplatePage() {
}}
showChart
/>
</div>
{/* 통합 팝업 */}
<WbsTemplateDialog
+4 -1
View File
@@ -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";