Skip to content

Commit

Permalink
docker-compose를 이용한 로컬 개발환경 구성, 문서 보강, color 벨리데이션 추가 (#112)
Browse files Browse the repository at this point in the history
* up: 버전 업

* doc: 기본 적인 설명 수정

* doc: api 호출 경로 변경

* up: 로컬 환경에서 다른 서비스와 포트가 겹침으로 변경

* up: 내용이 없는 경우 안내 표시 및 벨리데이션 보강

* add: local에서 docker으로 실행하는 설명 추가

* docs: 설명 보강

* up: url에 맞는 올바른 변수명 으로 교체

* fix: docker container 내부에서도 앱이 정상적으로 실행되도록 수정

* add: 이미지 완전히 업로드 여부 이모지 추가
  • Loading branch information
parkgang committed Dec 24, 2021
1 parent dd43c05 commit 2efe50c
Show file tree
Hide file tree
Showing 18 changed files with 155 additions and 51 deletions.
6 changes: 3 additions & 3 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
NEXT_PUBLIC_API_GATEWAY_URL=https://api-gateway.qa.belf.xyz
NEXT_PUBLIC_OAUTH_SERVER_URL=http://localhost:8080
NEXT_PUBLIC_STORAGE_SERVER_URL=https://storage.qa.belf.xyz
NEXT_PUBLIC_API_GATEWAY_URL=http://localhost:3000
NEXT_PUBLIC_OAUTH_SERVER_URL=http://localhost:3001
NEXT_PUBLIC_STORAGE_SERVER_URL=http://localhost:3004
17 changes: 17 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:14.16.1

# 앱 디렉터리 생성
WORKDIR /usr/src/app

# 앱 의존성 설치: package.json과 package-lock.json을 모두 복사하기 위해 와일드카드를 사용
COPY package*.json ./

RUN npm i

# 앱 소스 추가
COPY . .

# dev 환경으로 빌드
RUN npm run build:dev

CMD [ "npm", "run", "start" ]
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
# front-server

사용자의 코스를 공유하는 Todo 서비스 Web UI!
> 사용자의 코스를 공유하는 Todo 서비스 Web App!
1. Mobile UI를 Target으로 한 Web Application 입니다.
1. api-gateway를 이용하여 각각의 service를 호출합니다.
1. 반응형으로 고려된 Web Application 입니다.
1. [api-gateway](https://github.com/belf-kr/api-gateway) 등 필요한 service를 사용하여 belf service 사용할 수 있도록 합니다.
1. 와이어프레임은 XD를 통하여 디자인되었으며 [프로토타이핑](https://xd.adobe.com/view/ffec9bcc-87d9-4bc6-b873-721709411173-aabf) 에서 확인할 수 있습니다.

# Stack

1. node:v14.16.1
1. vscode
1. next.js
1. docker
1. axios
1. recoil
1. styled-components

# 빠른 시작

1. `npm i && npm run dev` 를 이용해 nextjs를 시작합니다.
## 개발 환경

1. `npm i` 으로 필요한 의존성을 설치합니다.
1. 필요에 따라 `.env.development` 에서 service의 호출 주소를 변경합니다.
1. local 환경에서 API를 호출하는 경우 사용하려는 API의 Server가 Up 되어있어야합니다.
1. API들은 [belf-kr/repositories](https://github.com/orgs/belf-kr/repositories) 에서 찾아보실 수 있으며 Server 실행 방법은 `README.md` 에 작성되어있습니다.
1. `npm run dev` 로 nextjs를 시작합니다.

## 제품 시작

> docker-compose으로 image build시 발생 이유를 모르는 `Error: Parsing error: Cannot destructure property 'isTypeVariable' of 'undefined' as it is undefined.` 에러가 발생하여 image build를 먼저하고 docker-compose으로 실행하도록 합니다.
```shell
docker build -t belf-front-server . --file=Dockerfile.dev
docker-compose up -d
```

위의 명령어를 입력해 docker image 생성 후 컨테이너를 생성합니다.

# 배포

`qa`, `prod` 으로 배포 환경이 구분되어 있으며 각 환경에 맞는 API의 Endpoint으로 호출해야 합니다.

이를 위해 배포 환경에 맞는 변수를 주입받아 image를 build 할 수 있도록 `.env.*``Dockerfile.*` 으로 분리되어 있습니다.

# 디렉터리 구조

Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: "3.7"
services:
front-server:
build:
context: ./
dockerfile: Dockerfile.dev
image: belf-front-server
container_name: front-server-dev
ports:
- 2999:3000
environment:
- SERVICE_RUNNING_IN_DOCKER=true
4 changes: 2 additions & 2 deletions domain/Course/CreateCourse/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function CreateCourse(): JSX.Element {
if (isPost) {
return;
}
if (!title) {
if (!title || !color) {
alert("입력되지 않은 값이 있습니다.");
return;
}
Expand All @@ -53,7 +53,7 @@ export default function CreateCourse(): JSX.Element {
</S.TitleBox>
<S.Contents>
<S.SubTitleBox>
<S.SubTitle>코스 색상 선택</S.SubTitle>
<S.SubTitle>* 코스 색상 선택</S.SubTitle>
</S.SubTitleBox>
<S.RadioColorsBox>
<SelectCourseColor colorOnChange={setColor} />
Expand Down
8 changes: 8 additions & 0 deletions domain/Course/SelectCourseColor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export default function SelectCourseColor({ colorOnChange }: props): JSX.Element
colorOnChange(currentColor);
}, [currentColor]);

if (courseColors.length === 0) {
return (
<>
<h4>컬러 정보가 없습니다.</h4>
</>
);
}

return (
<>
{courseColors.map((color, index) => {
Expand Down
5 changes: 4 additions & 1 deletion domain/Editer/EditImageArea/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function EditImageArea({ nodeItem, setNodeList }: props): JSX.Ele
const [imageFile, setImageFile] = useState<any>();

const [percentCompleted, setPercentCompleted] = useState("");
const [isCompleted, setIsCompleted] = useState<boolean>(false);

useEffect(() => {
hiddenFileInput.current.click();
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function EditImageArea({ nodeItem, setNodeList }: props): JSX.Ele
return x;
});
});
setIsCompleted(true);
setFileUploadDateTime(undefined);
setNodeList((prevNode) => prevNode.map((node) => (node.id === nodeItem.id ? nodeItem : node)));
})();
Expand All @@ -63,6 +65,7 @@ export default function EditImageArea({ nodeItem, setNodeList }: props): JSX.Ele
const file: File = event.target.files[0];

const upLoadNowTime = new Date();
setIsCompleted(false);
setFileUploadDateTime(upLoadNowTime);
setIsImgUploadEventQueue((prev) => {
return [
Expand Down Expand Up @@ -94,7 +97,7 @@ export default function EditImageArea({ nodeItem, setNodeList }: props): JSX.Ele
>
{imageFile?.file.name ?? ""}
</S.ImageNameText>
<S.ImageNameText>{percentCompleted ? `(${percentCompleted})` : ""}</S.ImageNameText>
<S.ImageNameText>{percentCompleted ? `(${percentCompleted}) ${isCompleted ? "✅" : "⏳"}` : ""}</S.ImageNameText>
</S.ImagePreviewBox>
</>
);
Expand Down
1 change: 1 addition & 0 deletions domain/Editer/EditImageArea/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ImageNameText = styled.a`
color: ${({ theme }) => theme.fontColor.sub};
font-size: ${({ theme }) => theme.common.fontSize.s200}px;
margin: auto 0px;
white-space: nowrap;
`;

export { Image, ImageInput, ImageNameText, ImagePreviewBox };
12 changes: 9 additions & 3 deletions domain/Editer/PostMaster/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ export default function PostMaster({ postDoneItem }: props): JSX.Element {
<S.TitleBox>
<S.Title>{postDoneItem.title}</S.Title>
</S.TitleBox>
<S.Body>
<PostNodeList nodeList={doneItem.content} />
</S.Body>
{doneItem.content.length === 0 ? (
<>
<h3>작성된 내용이 없습니다.</h3>
</>
) : (
<S.Body>
<PostNodeList nodeList={doneItem.content} />
</S.Body>
)}
</S.Box>
);
}
6 changes: 3 additions & 3 deletions libs/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import axios, { AxiosInstance } from "axios";

const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL;
export const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL;

export const apiClient: AxiosInstance = axios.create({
baseURL: `${apiGatewayUrl}/todo`,
export const apiGatewayClient: AxiosInstance = axios.create({
baseURL: `${apiGatewayUrl}`,
headers: {
"Content-Type": "application/json;charset=UTF-8",
},
Expand Down
4 changes: 2 additions & 2 deletions libs/colors/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { apiClient } from "../api-client";
import { apiGatewayClient } from "../api-client";

export async function getColors(): Promise<string[]> {
const { data } = await apiClient.get<string[]>(`/colors`);
const { data } = await apiGatewayClient.get<string[]>(`/todo/colors`);
return data;
}
42 changes: 30 additions & 12 deletions libs/course/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { apiClient } from "../api-client";
import { apiGatewayClient, apiGatewayUrl } from "../api-client";
import { CourseItem } from "../../types/components-type/course";
import { getLocalStorageAccessToken, TokenRefresh } from "../oauth";
import axios from "axios";

export async function postNewCourse(course: CourseItem): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.post(`/courses`, course, {
await apiGatewayClient.post(`/todo/courses`, course, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -30,7 +30,7 @@ export async function postNewCourse(course: CourseItem): Promise<void> {
}

export async function getCourses(userId?: number, courseId?: number, belfOnly?: boolean): Promise<CourseItem[]> {
const { data } = await apiClient.get<CourseItem[]>(`/courses`, {
const { data } = await apiGatewayClient.get<CourseItem[]>(`/todo/courses`, {
params: {
userId: userId,
courseId: courseId,
Expand All @@ -40,15 +40,24 @@ export async function getCourses(userId?: number, courseId?: number, belfOnly?:
return data;
}

export async function getCourse(courseId: number): Promise<CourseItem[]> {
const { data } = await apiClient.get<CourseItem[]>(`/courses/${courseId}`);
return data;
export async function getCourse(courseId: number, inDocker = false): Promise<CourseItem[]> {
const endpoint = `/todo/courses/${courseId}`;

if (inDocker) {
const { data } = await apiGatewayClient.get<CourseItem[]>(endpoint, {
baseURL: apiGatewayUrl.replace("localhost", "host.docker.internal"),
});
return data;
} else {
const { data } = await apiGatewayClient.get<CourseItem[]>(endpoint);
return data;
}
}

export async function deleteCourse(courseId: number): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.delete(`/courses/${courseId}`, {
await apiGatewayClient.delete(`/todo/courses/${courseId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -74,8 +83,8 @@ export async function deleteCourse(courseId: number): Promise<void> {
export async function postBelfCourse(courseId: number): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.post(
`/courses`,
await apiGatewayClient.post(
`/todo/courses`,
{ originalCourseId: courseId },
{
headers: {
Expand All @@ -101,7 +110,16 @@ export async function postBelfCourse(courseId: number): Promise<void> {
}
}

export async function getSearchCourses(keyword: string): Promise<CourseItem[]> {
const { data } = await apiClient.get<CourseItem[]>(`/courses/search?keyword=${encodeURI(keyword)}`);
return data;
export async function getSearchCourses(keyword: string, inDocker = false): Promise<CourseItem[]> {
const endpoint = `/todo/courses/search?keyword=${encodeURI(keyword)}`;

if (inDocker) {
const { data } = await apiGatewayClient.get<CourseItem[]>(endpoint, {
baseURL: apiGatewayUrl.replace("localhost", "host.docker.internal"),
});
return data;
} else {
const { data } = await apiGatewayClient.get<CourseItem[]>(endpoint);
return data;
}
}
12 changes: 6 additions & 6 deletions libs/todo/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { apiClient } from "../api-client";
import { apiGatewayClient } from "../api-client";

import { TodoItem } from "../../types/components-type/todo";
import { getLocalStorageAccessToken, TokenRefresh } from "../oauth";
import axios from "axios";

export async function getTodayTodos(userId?: number, courseId?: number, activeDate?: string, maximumActiveDate?: string): Promise<TodoItem[]> {
const { data } = await apiClient.get<TodoItem[]>("/work-todos", {
const { data } = await apiGatewayClient.get<TodoItem[]>("/todo/work-todos", {
params: {
userId: userId,
courseId: courseId,
Expand All @@ -17,14 +17,14 @@ export async function getTodayTodos(userId?: number, courseId?: number, activeDa
}

export async function getTodo(todoId: number): Promise<TodoItem> {
const { data } = await apiClient.get<TodoItem>(`/work-todos/${todoId}`);
const { data } = await apiGatewayClient.get<TodoItem>(`/todo/work-todos/${todoId}`);
return data;
}

export async function postNewTodo(todo: TodoItem): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.post<TodoItem>(`/work-todos`, todo, {
await apiGatewayClient.post<TodoItem>(`/todo/work-todos`, todo, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -50,7 +50,7 @@ export async function postNewTodo(todo: TodoItem): Promise<void> {
export async function deleteTodo(id: number): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.delete(`/work-todos/${id}`, {
await apiGatewayClient.delete(`/todo/work-todos/${id}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -76,7 +76,7 @@ export async function deleteTodo(id: number): Promise<void> {
export async function deleteUserTodoData(): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.delete(`/users`, {
await apiGatewayClient.delete(`/todo/users`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand Down
23 changes: 16 additions & 7 deletions libs/work-done/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiClient } from "../api-client";
import { apiGatewayClient, apiGatewayUrl } from "../api-client";

import { getLocalStorageAccessToken, TokenRefresh } from "../oauth";
import axios from "axios";
Expand All @@ -14,7 +14,7 @@ type WorkDone = {
export async function postWorkDone(workDone: WorkDone): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.post<WorkDone>(`/work-dones`, workDone, {
await apiGatewayClient.post<WorkDone>(`/todo/work-dones`, workDone, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand All @@ -36,13 +36,22 @@ export async function postWorkDone(workDone: WorkDone): Promise<void> {
}
}

export async function getDone(doneId: number): Promise<DoneItem> {
const { data } = await apiClient.get<DoneItem>(`/work-dones/${doneId}`);
return data;
export async function getDone(doneId: number, inDocker = false): Promise<DoneItem> {
const endpoint = `/todo/work-dones/${doneId}`;

if (inDocker) {
const { data } = await apiGatewayClient.get<DoneItem>(endpoint, {
baseURL: apiGatewayUrl.replace("localhost", "host.docker.internal"),
});
return data;
} else {
const { data } = await apiGatewayClient.get<DoneItem>(endpoint);
return data;
}
}

export async function getDones(courseId?: number): Promise<DoneItem[]> {
const { data } = await apiClient.get<DoneItem[]>("/work-dones", {
const { data } = await apiGatewayClient.get<DoneItem[]>("/todo/work-dones", {
params: {
courseId: courseId,
},
Expand All @@ -53,7 +62,7 @@ export async function getDones(courseId?: number): Promise<DoneItem[]> {
export async function deleteDone(id: number): Promise<void> {
async function work() {
const accessToken = getLocalStorageAccessToken();
await apiClient.delete(`/work-Dones/${id}`, {
await apiGatewayClient.delete(`/todo/work-Dones/${id}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2efe50c

Please sign in to comment.