Skip to content

[재하] 1205(화) 개발기록

박재하 edited this page Dec 5, 2023 · 3 revisions

목표

어드민 페이지 Full Stack, 전체 글 조회기능 및 시스템 정보 조회기능 구현

  • emotion 설치
  • Button, Table 컴포넌트 생성
  • 전체 글 조회하기
    • 전체 글 조회 API 구현
    • Board 컴포넌트 생성, 전체 글 조회
    • 트러블 슈팅: Vite dev/prod 환경변수 설정
    • 테스트 (동작 화면)
  • 시스템 정보 조회하기
    • 시스템 정보 조회 API 구현 (GET /admin/system-info)
    • SystemInfo 페이지 구현, 라우팅 (/admin/system-info)
    • 테스트 (동작 화면)
  • disk usage 조회, 글 상세조회
    • disk usage 조회
    • 글 상세조회
    • 테스트 (동작 화면)

emotion 설치

yarn workspace admin add @emotion/styled @emotion/react

프론트 프로젝트와 의존성을 동일하게 가기 위해 @emotion/styled, @emotion/react를 설치해준다.

Button, Table 컴포넌트 생성

버튼, 테이블 등 공유해서 사용할 수 있는 베이스 컴포넌트들을 생성해준다. 학습메모 2를 참고하여 프론트와 동일한 코드 컨벤션으로 구현을 해보았다.

// Button.tsx
import styled from '@emotion/styled';

interface PropsType extends React.ButtonHTMLAttributes<HTMLButtonElement> {
	onClick?: () => void;
	children: React.ReactNode;
}

export default function Button({ children, ...args }: PropsType) {
	return <CustomButton {...args}>{children}</CustomButton>;
}

const CustomButton = styled.button<PropsType>`
	background-color: #fff;
	border: 1px solid #ddd;
	border-radius: 4px;
	color: #212121;
	font-size: 14px;
	font-weight: 600;
	padding: 6px 12px;
	cursor: pointer;
	outline: none;
	&:hover {
		background-color: #ddd;
	}
`;
// Table.tsx
import styled from '@emotion/styled';

interface PropsType extends React.TableHTMLAttributes<HTMLTableElement> {
	children: React.ReactNode;
}

export default function Table({ children, ...args }: PropsType) {
	return <CustomTable {...args}>{children}</CustomTable>;
}

const CustomTable = styled.table<PropsType>`
	border-collapse: collapse;
	width: 100%;
	border: 1px solid #ddd;
`;

스타일은 추후에 또 적당히 수정해보자.

전체 글 조회 API 구현

백엔드에 전체 글 조회 API가 없기 때문에 만들어둔 admin 모듈에 새로운 API를 만들어준다.

// admin.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AdminService } from './admin.service';

@Controller('admin')
export class AdminController {
	constructor(private readonly adminService: AdminService) {}

	@Get('post')
	getAllPosts() {
		return this.adminService.getAllPosts();
	}
}
// admin.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/auth/entities/user.entity';
import { Board } from 'src/board/entities/board.entity';
import { Repository } from 'typeorm';

@Injectable()
export class AdminService {
	constructor(
		@InjectRepository(User)
		private readonly userRepository: Repository<User>,
		@InjectRepository(Board)
		private readonly boardRepository: Repository<Board>,
	) {}

	async getAllPosts() {
		const posts = await this.boardRepository.find();
		return posts;
	}
}

Board 컴포넌트 생성, 전체 글 조회

board 메뉴에 보여줄 페이지를 컴포넌트로 만들어준다. 마찬가지로 학습메모 5, 프론트 분들의 코드를 참고하여 동일한 컨벤션으로 구현 시도.

import { useState } from 'react';
import Button from '../shared/Button';
import Table from '../shared/Table';

const baseUrl = import.meta.env.VITE_API_BASE_URL;

export default function Board() {
	const [boardList, setBoardList] = useState([]);

	const getBoardList = async () => {
		const response = await fetch(baseUrl + '/admin/post');
		const data = await response.json();
		setBoardList(data);
	};

	return (
		<div>
			<Button onClick={getBoardList}>게시글 불러오기</Button>
			<Table>
				<thead>
					<tr>
						<th>번호</th>
						<th>제목</th>
						<th>작성자</th>
						<th>좋아요</th>
						<th>이미지 </th>
						<th>작성일시</th>
						<th>수정일시</th>
					</tr>
				</thead>
				<tbody>
					{boardList.map((board: any) => (
						<tr key={board.id}>
							<td>{board.id}</td>
							<td>{board.title}</td>
							<td>{board.user.nickname}</td>
							<td>{board.like_cnt}</td>
							<td>{board.images.length}</td>
							<td>{board.created_at}</td>
							<td>{board.updated_at}</td>
						</tr>
					))}
				</tbody>
			</Table>
		</div>
	);
}

Nav바 리팩토링

완성된 페이지는 라우트와 Nav바에 등록해줘야 한다.

Nav바는 아래와 같이 리팩토링해서 App.tsx에서 메뉴를 한번에 편집이 가능하도록 변경했다.

// Nav.tsx
import styled from '@emotion/styled';

interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
	children: React.ReactNode;
}

export default function Nav({ children, ...args }: PropsType) {
	return <CustomNav {...args}>{children}</CustomNav>;
}

// 최상단에 위치하도록 설정
const CustomNav = styled.nav<PropsType>`
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 20px;

	display: flex;
	align-items: center;
	justify-content: space-around;
	background-color: #fff;
	border-bottom: 1px solid #ddd;
	padding: 12px 12px;
	& > a {
		margin-right: 16px;
		text-decoration: none;
		color: #212121;
		font-size: 14px;
		font-weight: 600;
	}
	& > a:hover {
		text-decoration: underline;
	}
`;

App.tsx에서 라우트와 Nav바에 Board 등록

이제 최종적으로 board 페이지 등록.

// App.tsx
import { Route, Routes } from 'react-router-dom';
import './App.css';
import { TestComponent } from './components/TestComponent/TestComponent.tsx';

import Board from './components/Board/Board.tsx';
import Nav from './components/Nav.tsx';

function App() {
	return (
		<>
			<Nav>
				<a href="/">Home</a>
				<a href="/about">About</a>
				<a href="/abc">Test</a>
				<a href="/board">Board</a>
			</Nav>
			<Routes>
				<Route path="/" element={<div>Home</div>} />
				<Route path="/about" element={<div>About</div>} />
				<Route path="/abc" element={<TestComponent />} />
				<Route path="/board" element={<Board />} />
			</Routes>
		</>
	);
}

export default App;

트러블 슈팅: Vite dev/prod 환경변수 설정

board 페이지에서 게시글 전체 조회를 위한 fetch 시 baseUrl을 개발 환경에서는 http://localhost:3000, 배포 환경에서는 https://www.별글.site/api로 해줘야 하는데,

기존에 알고 있던 REACT 환경변수가 먹히지 않아 애를 좀 먹었다. 결국 본 프로젝트에서는 Vite를 이용하여 dev서버 실행과 build를 진행하므로, Vite에서 제공하는 환경변수 import 형태를 준수해야 했다.

학습메모 4를 참고하여 admin workspace 루트인 /packages/admin/.env.development, .env.production 두 파일을 추가해준다.

VITE_API_BASE_URL=http://localhost:3000
VITE_API_BASE_URL=https://www.별글.site/api

불러올 땐 아래와 같이 불러오면 된다.

const baseUrl = import.meta.env.VITE_API_BASE_URL;

테스트 (동작 화면)

yarn workspace admin dev
스크린샷 2023-12-05 오후 1 57 41

이제 dev서버에서 요청하면 http://localhost:3000로 잘 가고

yarn workspace admin build
스크린샷 2023-12-05 오후 1 10 06

빌드해보면 https://www.별글.site/api이 잘 등록되어 있음을 확인할 수 있다.

스크린샷 2023-12-05 오후 1 25 53

전체적인 실행 화면은 위와 같다.

시스템 정보 조회 API 구현 (GET /admin/system-info)

yarn workspace server add os-utils

다양한 방법이 있지만, os-utils 패키지를 이용해 조회하는 것이 가장 직관적이라 판단되어 해당 솔루션을 활용함

@Get('system-info')
getSystemInfo() {
  return this.adminService.getSystemInfo();
}

컨트롤러 단에 GET /admin/system-info를 추가해주고,

async getSystemInfo() {
  // 플랫폼 정보
  const platform = osUtils.platform();

  // CPU 개수
  const cpuCount = osUtils.cpuCount();

  // cpu 사용량 (비동기로 동작하여 Promise로 감싸줌)
  const usedCpu: number = await new Promise((resolve) => {
    osUtils.cpuUsage((cpuUsage) => {
      resolve(cpuUsage);
    });
  });
  const freeCpu: number = await new Promise((resolve) => {
    osUtils.cpuFree((cpuFree) => {
      resolve(cpuFree);
    });
  });
  const cpuUsage = `${Math.floor(usedCpu * 100 * 100) / 100}% (free ${
    Math.floor(freeCpu * 100 * 100) / 100
  }%)`;

  // 메모리 사용량
  const totalMem = osUtils.totalmem();
  const freeMem = osUtils.freemem();
  const usedMem = totalMem - freeMem;
  const memUsagePercent = usedMem / totalMem;
  const memUsage = `${Math.floor(memUsagePercent * 100 * 100) / 100}%`;

  // 시스템 정보 객체로 반환
  const systemInfo = {
    platform,
    cpuCount,
    cpuUsage,
    memUsage,
  };

  return systemInfo;
}

서비스 단에서 로직을 구현해줬다.

cpuUsage, cpuFree 메소드는 콜백 형태로만 반환이 되기 때문에, 기다린 후에 값을 넣어주기 위해 Promise로 감싸서 await 시켜줬다.

이 부분에서 조금 시간이 쓰였음

SystemInfo 페이지 구현, 라우팅 (/admin/system-info)

import { useState } from 'react';
import Button from '../shared/Button';
import Table from '../shared/Table';

const baseUrl = import.meta.env.VITE_API_BASE_URL;

export default function SystemInfo() {
	const [systemInfo, setSystemInfo]: any = useState([]);

	const getSystemInfo = async () => {
		const response = await fetch(baseUrl + '/admin/system-info');
		const data = await response.json();
		setSystemInfo(data);
	};

	return (
		<div>
			<Button onClick={getSystemInfo}>시스템 정보 불러오기</Button>
			<Table>
				<thead>
					<tr>
						<th>플랫폼</th>
						<th>CPU 수</th>
						<th>CPU 사용량</th>
						<th>메모리 사용량</th>
					</tr>
				</thead>
				<tbody>
					<tr>
						<td>{systemInfo.platform}</td>
						<td>{systemInfo.cpuCount}</td>
						<td>{systemInfo.cpuUsage}</td>
						<td>{systemInfo.memUsage}</td>
					</tr>
				</tbody>
			</Table>
		</div>
	);
}

Board와 유사하게 페이지 컴포넌트를 만들어 줬다. 클릭 시마다 현재 시스템 상태정보를 다시 조회할 수 있도록!

function App() {
	return (
		<>
			<Nav>
				<a href="/admin">Home</a>
				<a href="/admin/about">About</a>
				<a href="/admin/abc">Test</a>
				<a href="/admin/board">Board</a>
				<a href="/admin/system-info">System Info</a>
			</Nav>
			<Routes>
				<Route path="/admin" element={<div>Home</div>} />
				<Route path="/admin/about" element={<div>About</div>} />
				<Route path="/admin/abc" element={<TestComponent />} />
				<Route path="/admin/board" element={<Board />} />
				<Route path="/admin/system-info" element={<SystemInfo />} />
			</Routes>
		</>
	);
}

마지막으로 라우트와 Nav바에 등록해주면 완성

테스트 (동작 화면)

스크린샷 2023-12-05 오후 3 55 36 스크린샷 2023-12-05 오후 3 53 35

클릭할 때마다 정상적으로 값을 받아옴을 확인할 수 있다.

disk usage 조회

간략히만 기록하겠다. 일단 메모리 말고 디스크 사용량 확인이 없어서 찝찝해서 추가해봤다.

별도로 패키지 추가는 필요없고 child_process 내장모듈만으로 구현 가능했다.

// admin.service.ts
// 디스크 사용량
const diskUsageString: string = await new Promise((resolve) => {
	exec('df -h', (error, stdout, stderr) => {
		resolve(stdout);
	});
});
const diskUsageRows = diskUsageString.split('\n');
const diskUsage = [];
diskUsageRows.forEach((row) => {
	const rowSplit = row.split(' ');
	const rowSplitFiltered = rowSplit.filter((item) => item !== '');
	diskUsage.push(rowSplitFiltered);
});
// header는 따로 전송
const diskUsageHead = diskUsage.shift();

좋고~ 이걸 출력해주기 위해 프론트 단에선 테이블을 하나 더 만들어 출력했다.

// SystemInfo.tsx
...
export default function SystemInfo() {
	...
	return (
		<div>
			...
			<Table>
				<thead>
					<tr>
						{systemInfo.diskUsageHead &&
							(systemInfo.diskUsageHead as any).map(
								(head: string, index: number) => <th key={index}>{head}</th>,
							)}
					</tr>
				</thead>
				<tbody>
					{systemInfo.diskUsage &&
						(systemInfo.diskUsage as any).map(
							(line: string[], index: number) => {
								return (
									<tr key={index}>
										{line.map((item: string, index: number) => (
											<td key={index}>{item}</td>
										))}
									</tr>
								);
							},
						)}
				</tbody>
			</Table>
		</div>
	);
}

글 상세조회

글 상세조회도 만들어보자.

async getAllPosts() {
	const posts = await this.boardRepository.find();

	// 컨텐츠 복호화
	posts.forEach((post) => {
		post.content = decryptAes(post.content);
	});

	// 이미지 있는 경우 이미지 경로 추가
	posts.forEach((post: any) => {
		if (post.images.length > 0) {
			post.images = post.images.map(
				(image) =>
					`${awsConfig.endpoint.href}${bucketName}/${image.filename}`,
			);
		}
	});

	// console.log(posts);

	return posts;
}

상세조회를 위해 본문은 복호화하고, 이미지는 링크정보를 가져오도록 변환했다.

다음으로 프론트

원래 파라미터가 없는 button 컴포넌트를 event 파라미터 넣도록 만들고, closest를 사용해 tr의 id값을 입력받도록 개선했다. id값 하나 받아오려고 고생 쫌 했다...

// Board.tsx
...
export default function Board() {
	...
	const getBoardDetail = async (e: React.MouseEvent<HTMLButtonElement>) => {
		// 이벤트 위임으로 선택한 tr의 id값을 가져온다.
		const id = Number((e.target as any).closest('tr').id);
		const data = boardList.find((board: any) => board.id === id);
		setBoardDetail(data as any);
		console.log(boardDetail);
	};

	return (
		<div>
			<Button onClick={getBoardList}>게시글 불러오기</Button>
			<Table>
				<thead>
					...
				</thead>
				<tbody>
					{boardList.map((board: any) => (
						<tr key={board.id} id={board.id}>
							...
							<td>
								<Button onClick={(e) => getBoardDetail(e)}>상세 보기</Button>
							</td>
						</tr>
					))}
				</tbody>
			</Table>
			<div>
				{boardDetail &&
					(Object.keys(boardDetail) as any).map((detail: any) => {
						return (
							<div>
								<div>{detail + ' | ' + boardDetail[detail]}</div>
							</div>
						);
					})}
			</div>
		</div>
	);
}

시간나면 상세보기는 카드나 모달 형태로 만들고, 이미지 태그로 이미지도 직접 보여주도록 변경해봐도 좋을듯.

테스트 (동작 화면)

디스크 사용량

스크린샷 2023-12-05 오후 11 36 51

게시글 상세 보기

스크린샷 2023-12-05 오후 11 36 35

학습메모

  1. [React] emotion과 styled-component의 차이
  2. button 컴포넌트 예시
  3. REACT env 사용
  4. vite env 사용
  5. 페이지 컴포넌트 예시
  6. os-utils를 활용한 시스템자원 사용량 체크
  7. os-utils 공식repo
  8. process 모듈 사용한 시스템자원 확인

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally