Skip to content

Commit 29600f0

Browse files
authored
feat(view): 스토리라인 필터 릴리즈/기여자 필터링 UI, 기능 구현 (#1027)
* feat: 기여자, 릴리즈 필터 추가 * feat: 스토리라인 필터 적용 * refactor: 스토리라인 차트 필터 스타일 변경 및 코드 주석 제거
1 parent e05483a commit 29600f0

File tree

9 files changed

+431
-30
lines changed

9 files changed

+431
-30
lines changed

packages/view/src/components/StorylineChart/ReleaseVisualization.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
calculateReleaseNodePosition,
88
findFirstReleaseContributorNodes,
99
generateReleaseFlowLineData,
10-
generateReleaseFlowLinePath,
1110
} from "./StorylineChart.util";
1211

1312
/**
@@ -297,7 +296,34 @@ export const renderReleaseVisualization = ({
297296
.enter()
298297
.append("path")
299298
.attr("class", "flow-line")
300-
.attr("d", (d) => generateReleaseFlowLinePath(d, xScale, yScale))
299+
.attr("d", (d) => {
300+
// 실제 노드 위치를 찾아서 경로 생성 (날짜까지 매칭)
301+
const startActivity = releaseContributorActivities.find(
302+
(a) =>
303+
a.releaseIndex === d.startReleaseIndex &&
304+
a.folderPath === d.startFolder &&
305+
a.contributorName === d.contributorName &&
306+
a.date.getTime() === d.startDate.getTime()
307+
);
308+
const endActivity = releaseContributorActivities.find(
309+
(a) =>
310+
a.releaseIndex === d.endReleaseIndex &&
311+
a.folderPath === d.endFolder &&
312+
a.contributorName === d.contributorName &&
313+
a.date.getTime() === d.endDate.getTime()
314+
);
315+
316+
if (!startActivity || !endActivity) {
317+
return "";
318+
}
319+
320+
const x1 = calculateReleaseNodePosition(startActivity, xScale, activitiesByRelease);
321+
const y1 = (yScale(d.startFolder) || 0) + yScale.bandwidth() / 2;
322+
const x2 = calculateReleaseNodePosition(endActivity, xScale, activitiesByRelease);
323+
const y2 = (yScale(d.endFolder) || 0) + yScale.bandwidth() / 2;
324+
const midX = (x1 + x2) / 2;
325+
return `M ${x1},${y1} Q ${midX},${y1} ${midX},${(y1 + y2) / 2} Q ${midX},${y2} ${x2},${y2}`;
326+
})
301327
.attr("fill", "none")
302328
.attr("stroke", (d) => colorScale(d.contributorName) as string)
303329
.attr("stroke-width", 2)

packages/view/src/components/StorylineChart/StorylineChart.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
font-weight: $font-weight-regular;
6666
}
6767

68+
&__filters {
69+
margin-top: 0.5rem;
70+
padding: 1rem;
71+
}
72+
6873
&__breadcrumb {
6974
display: flex;
7075
align-items: center;

packages/view/src/components/StorylineChart/StorylineChart.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import NavigateNextIcon from "@mui/icons-material/NavigateNext";
77
import WorkspacePremiumRoundedIcon from "@mui/icons-material/WorkspacePremiumRounded";
88
import { Link } from "@mui/material";
99

10-
import { useDataStore } from "store";
10+
import { useDataStore, useStorylineFilterStore } from "store";
1111

1212
import { DIMENSIONS, getResponsiveChartWidth } from "./StorylineChart.const";
1313
import "./StorylineChart.scss";
1414
import { extractReleaseBasedContributorActivities } from "./StorylineChart.util";
1515
import { renderReleaseVisualization } from "./ReleaseVisualization";
1616
import { useFolderNavigation } from "./useFolderNavigation";
17+
import StorylineFilters from "./StorylineFilters";
1718

1819
const StorylineChart = () => {
1920
const [totalData] = useDataStore(useShallow((state) => [state.data]));
21+
const { releaseRange, selectedContributors } = useStorylineFilterStore();
2022

2123
const svgRef = useRef<SVGSVGElement>(null);
2224
const tooltipRef = useRef<HTMLDivElement>(null);
@@ -39,6 +41,41 @@ const StorylineChart = () => {
3941

4042
const breadcrumbs = useMemo(() => getBreadcrumbs(), [getBreadcrumbs]);
4143

44+
// 원본 활동 데이터 계산
45+
const releaseContributorActivities = useMemo(() => {
46+
if (!totalData || totalData.length === 0 || releaseTopFolderPaths.length === 0) {
47+
return [];
48+
}
49+
50+
const currentDepth = currentPath === "" ? 1 : currentPath.split("/").length + 1;
51+
return extractReleaseBasedContributorActivities(totalData, releaseTopFolderPaths, currentDepth);
52+
}, [totalData, releaseTopFolderPaths, currentPath]);
53+
54+
// 필터링된 activities 계산
55+
const filteredActivities = useMemo(() => {
56+
if (releaseContributorActivities.length === 0) {
57+
return [];
58+
}
59+
60+
let filtered = [...releaseContributorActivities];
61+
62+
// 릴리즈 범위 필터
63+
if (releaseRange) {
64+
filtered = filtered.filter(
65+
(activity) =>
66+
activity.releaseIndex >= releaseRange.startReleaseIndex &&
67+
activity.releaseIndex <= releaseRange.endReleaseIndex
68+
);
69+
}
70+
71+
// 기여자 필터
72+
if (selectedContributors.length > 0) {
73+
filtered = filtered.filter((activity) => selectedContributors.includes(activity.contributorName));
74+
}
75+
76+
return filtered;
77+
}, [releaseContributorActivities, releaseRange, selectedContributors]);
78+
4279
useEffect(() => {
4380
const updateWidth = () => {
4481
const containerWidth = containerRef.current?.clientWidth;
@@ -66,20 +103,6 @@ const StorylineChart = () => {
66103
}, []);
67104

68105
const { topContributorName, releaseRangeLabel } = useMemo(() => {
69-
if (!totalData || totalData.length === 0 || releaseTopFolderPaths.length === 0) {
70-
return {
71-
topContributorName: null,
72-
releaseRangeLabel: "...",
73-
};
74-
}
75-
76-
const currentDepth = currentPath === "" ? 1 : currentPath.split("/").length + 1;
77-
const releaseContributorActivities = extractReleaseBasedContributorActivities(
78-
totalData,
79-
releaseTopFolderPaths,
80-
currentDepth
81-
);
82-
83106
if (releaseContributorActivities.length === 0) {
84107
return {
85108
topContributorName: null,
@@ -125,7 +148,7 @@ const StorylineChart = () => {
125148
topContributorName: mostActiveContributor || null,
126149
releaseRangeLabel: rangeLabel || "...",
127150
};
128-
}, [totalData, releaseTopFolderPaths, currentPath]);
151+
}, [releaseContributorActivities]);
129152

130153
useEffect(() => {
131154
if (!totalData || totalData.length === 0) {
@@ -142,17 +165,9 @@ const StorylineChart = () => {
142165

143166
const svg = d3.select(svgRef.current).attr("width", chartWidth).attr("height", DIMENSIONS.height);
144167

145-
// activity가 있는 폴더 카운트
146-
const currentDepth = currentPath === "" ? 1 : currentPath.split("/").length + 1;
147-
const releaseContributorActivities = extractReleaseBasedContributorActivities(
148-
totalData,
149-
releaseTopFolderPaths,
150-
currentDepth
151-
);
152-
153168
svg.selectAll("*").remove();
154169

155-
if (releaseContributorActivities.length === 0) {
170+
if (filteredActivities.length === 0) {
156171
svg
157172
.append("text")
158173
.attr("x", chartWidth / 2)
@@ -167,12 +182,12 @@ const StorylineChart = () => {
167182

168183
renderReleaseVisualization({
169184
svg,
170-
releaseContributorActivities,
185+
releaseContributorActivities: filteredActivities,
171186
releaseTopFolderPaths,
172187
tooltipRef,
173188
onFolderClick: navigateToFolder,
174189
});
175-
}, [totalData, releaseGroups, releaseTopFolderPaths, navigateToFolder, currentPath, chartWidth]);
190+
}, [filteredActivities, releaseTopFolderPaths, navigateToFolder, chartWidth, totalData, releaseGroups]);
176191

177192
const topContributorLabel = topContributorName || "...";
178193

@@ -218,6 +233,9 @@ const StorylineChart = () => {
218233

219234
<div className="storyline-chart__subtitle">{releaseRangeLabel}</div>
220235
</div>
236+
<div className="storyline-chart__filters">
237+
{releaseContributorActivities.length > 0 && <StorylineFilters activities={releaseContributorActivities} />}
238+
</div>
221239

222240
<svg
223241
className="storyline-chart__chart"

packages/view/src/components/StorylineChart/StorylineChart.type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export interface ReleaseContributorActivity {
3232
export interface ReleaseFlowLineData {
3333
startReleaseIndex: number;
3434
startFolder: string;
35+
startDate: Date;
3536
endReleaseIndex: number;
3637
endFolder: string;
38+
endDate: Date;
3739
contributorName: string;
3840
}

packages/view/src/components/StorylineChart/StorylineChart.util.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,25 @@ export function generateReleaseFlowLineData(
7575
const current = activities[i];
7676
const next = activities[i + 1];
7777

78-
if (current.releaseIndex !== next.releaseIndex) {
78+
// 같은 릴리즈, 같은 폴더, 같은 시간이 아닌 경우 선을 그림
79+
// (오프셋이 다른 경우 = 다른 릴리즈, 다른 폴더, 또는 다른 시간)
80+
const isSamePosition =
81+
current.releaseIndex === next.releaseIndex &&
82+
current.folderPath === next.folderPath &&
83+
current.date.getTime() === next.date.getTime();
84+
85+
// 릴리즈가 연속적인지 확인 (같은 릴리즈이거나 바로 다음 릴리즈인 경우만)
86+
const isConsecutiveRelease =
87+
current.releaseIndex === next.releaseIndex || current.releaseIndex + 1 === next.releaseIndex;
88+
89+
if (!isSamePosition && isConsecutiveRelease) {
7990
flowLineData.push({
8091
startReleaseIndex: current.releaseIndex,
8192
startFolder: current.folderPath,
93+
startDate: current.date,
8294
endReleaseIndex: next.releaseIndex,
8395
endFolder: next.folderPath,
96+
endDate: next.date,
8497
contributorName: current.contributorName,
8598
});
8699
}

0 commit comments

Comments
 (0)