학교 개발 동아리 Megabrain에서 각 동아리원들 코딩 실태 조사 느낌으로 도입했던 Wakatime이라는 서비스가 있었다.
wakatime은 개발 시간 측정 서비스로 각 개발 ide 플러그인으로 구현되어 있다. 모든 동아리원이 플러그인을 적용하여 매주 얼마나 개발을 했는지 확인 용도로 서비스를 사용하고 있었다.
매주 불편하게 디스코드 스레드를 열고, 캡처 후 업로드하는 방식이 불편했던 나는 Wakatime에서 제공하는 API를 가지고 express를 사용하여 백엔드를 구축하고 React로 프론트를 간단하게 구축하였다.
백엔드 제작기는 아래 글을 참고하면 된다.
레이아웃부터 잡아보자
먼저 emotion, mui를 사용하여 디자인 및 UI Component를 사용하였다. 모바일 대응을 위해여 Header Component를 만들어주고 모바일 사이즈에서는 Drawer로 구현되도록 컴포넌트를 만들어주었다.
const BoardHeader = () => {
const [mobileOpen, setMobileOpen] = useState(false); //모바일 상태에서 Drawer 열었는지 유무
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
return (
<Box>
<AppBar component="nav">
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: "none" } }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
component="div"
sx={{ flexGrow: 1, display: { xs: "none", sm: "block" } }}
>
Mega Waka Board
</Typography>
<Box sx={{ display: { xs: "none", sm: "block" } }}>
{navItems.map((item) => ( // 헤더 아이템 개수만큼 버튼으로 만들어주기
<Link
to={item.route}
key={item.name}
style={{ textDecoration: "none" }}
>
<Button sx={{ color: "#fff" }}>{item.name}</Button>
</Link>
))}
</Box>
</Toolbar>
</AppBar>
<Box component="nav">
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: "block", sm: "none" },
"& .MuiDrawer-paper": { boxSizing: "border-box", width: 240 },
}}
>
<HeaderDrawer handleDrawerToggle={() => handleDrawerToggle} /> // Drawer Component 불러오기
</Drawer>
</Box>
</Box>
);
};
위 화면은 데스크톱 사이즈일 때 헤더 Layout이고 아래는 모바일 사이즈일 때 햄버거 메뉴를 눌렀을 때 나오는 Drawer Component를 보여주었다.
Leader Board 만들기
백엔드에서 만들어 뒀던 Swagger를 토대로 총 4가지 API를 Axios를 사용하여 Get or Post 요청을 할 수 있도록 apis 폴더에 만들어주었다.
해당 api들 중 getAllMemberWaka를 Component가 생성될 때 각 유저마다 name, organization, 7 days_time, 14 days_time, 30 days_time을 한 번에 받아오도록 하였다.
import getAllMemberWaka from "../../apis/getAllMemberWaka";
const [memberData, setMemberData] = useState({ data: [], isLoading: true });
useEffect(() => {
getAllMemberWaka().then((res) => {
setMemberData({ data: res, isLoading: false });
});
}, []);
그리고 state로 저장된 memberData들을 한 번에 보여주는 것보단 7일, 14일, 30일 단위로 끊어서 보여주는 것이 좋다고 판단하여 MUI의 Button Group 컴포넌트를 사용하여 7일, 14일, 30일 단위로 데이터를 보여주도록 제작하였다.
const [day, setDay] = useState(7);
const handleChange = (
event: React.MouseEvent<HTMLElement>,
newDay: number | null
) => {
if (newDay != null) setDay(newDay);
};
<ToggleButtonGroup
color="primary"
value={day}
exclusive
onChange={handleChange}
>
<ToggleButton value={7}>지난 7일</ToggleButton>
<ToggleButton value={14}>지난 14일</ToggleButton>
<ToggleButton value={30}>지난 30일</ToggleButton>
</ToggleButtonGroup>
각 버튼을 누를 때마다 Day State가 변경되도록 만들었고, State가 변경될 때 LeaderBoardList 컴포넌트에서 밑에 뿌려주는 데이터 변경하면서, 순위별로 정렬할 수 있게 제작하였다.
const knowDaysValue = (data) => {
return data.sort((a: dto, b: dto) => {
const sortA = dayReplaceFunc(a);
const sortB = dayReplaceFunc(b);
if (sortA.indexOf(":") === -1) return 1;
if (sortB.indexOf(":") === -1) return -1;
return parseInt(sortB.split(":")[0]) < parseInt(sortA.split(":")[0])
? -1
: parseInt(sortB.split(":")[0]) > parseInt(sortA.split(":")[0])
? 1
: parseInt(sortB.split(":")[1]) < parseInt(sortA.split(":")[1])
? -1
: 1;
});
};
위 코드는 데이터의 시간 순서대로 정렬하는 함수를 만들어주었다.
<img
src={`https://avatars.dicebear.com/api/croodles-neutral/${value.username}.svg`}
alt=""
style={{ width: 80, borderRadius: "50%" }}
/>
각 사람당 프로필 사진은 avatars api를 사용하여 username 값을 키 값으로 두어 같은 캐릭터가 나오지 않도록 해주었다.
그리고 1,2,3등 메달은 png로 첨부하니 성능 측정하는 lighthouse를 돌려보니 로딩 속도가 너무 느리다고 나오게 되어, webp로 변환하여 출력해 주었다.
세부 페이지 만들기
원래는 계획에 없던 세부 페이지였지만, 어떤 프로젝트를, 어떤 언어를, 어떤 ide를 사용했는지 여부도 Wakatime API에서 제공해 주고 있었고 일별로도 데이터를 보내주고 있었기 때문에 백엔드에서 API를 추가하여 각 사람당 하나의 세부 페이지를 만들도록 해보았다.
<Route path="/user/:day/:id" element={<UserBoard />} />
react-router-dom을 이용하여 각 유저의 id와 day마다 다른 세부페이지를 보일 수 있도록 제작하였다.
https://waka.megabrain.kr/user/7day/26
id 26번은 내 id이다. 따라서 해당 url에선 7일 동안의 내 데이터를 볼 수 있는 세부 페이지이다.
chart.js를 사용하여 데이터를 chart.js에 맞는 데이터 set으로 변환하였다.
const parseBarChartWeek = (data) => {
const newData = {
labels: data.weekData.label,
datasets: [
{
label: "Hours",
data: data.weekData.data.map((item) => (item / 3600).toFixed(2)),
},
],
};
return newData;
};
<Bar options={options} data={parseBarChartWeek(data)} />
언어와 프로젝트를 한눈에 볼 수 있도록 chart.js에 Doughnut을 사용하여 한 눈에 볼 수 있게 하고, Table로도 한눈에 볼 수 있게 제작하였다.
const parseChartLanguages = (data) => {
data.languages.sort((a, b) => b.seconds - a.seconds);
let newData = {
labels: data.languages.map((item) => item.name),
datasets: [
{
label: " Hours",
data: data.languages.map((item) => (item.seconds / 3600).toFixed(2)),
},
],
};
return newData;
};
<Grid item xs={5}>
<Typography variant="h5" textAlign="center" sx={{ mb: 2 }}>
Languages
</Typography>
<Doughnut data={parseChartLanguages(data)} />
</Grid>
<Grid item xs={5}>
<TableContainer>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>언어 (Language)</TableCell>
<TableCell>시간 (Time)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.languages.map((row) => (
<TableRow key={row.name}>
<TableCell>{row.name}</TableCell>
<TableCell>{timeParser(row.seconds)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
결론
Mega Waka Board는 동아리방 K3S에 올라가 있다. 현재는 도메인 발급도 되어 있어 이 곳으로 들어오면 사이트를 확인해 볼 수 있다.
https://github.com/inje-megabrain/Mega-Waka-Board-fe