본문 바로가기

Back-End

프로그래머스 우리싸인 API 서버 개발 과제분석

728x90
 

Stack

NodeJs,Typescript,node-json-db

 

Preview

이번 글은 프로그래머스 과제 테스트 란에 있는 우리 싸인 API 개발 과제에 대한 분석 및 공부를 위해 작성되었습니다. 과제 대략적인 요건은 이렇습니다. 회원가입/로그인/참가자 인증을 하고 문서에 서명하여 데이터베이스에 저장 및 조회 등의 처리를 위한 Back end 로직을 개발합니다. 자세한 요건은 아래 더 보기를 클릭하시면 볼 수 있습니다.

https://school.programmers.co.kr/skill_check_assignments/233

더보기
# 우리싸인 API 서버

 

문서를 관리하고 참가자가 문서에 서명이 가능한 `우리싸인 API 서버`를 개발해야 합니다. `공개용 API(가입/로그인/참가자 인증)`는 개발이 되어 있는 상태입니다.

 

개발 요건을 잘 확인하고, 우리싸인 API 서버를 완성해주세요.

 

## 개발 환경

 

- 개발 언어: JavaScript, TypeScript v4.3
- 실행 환경: Node.js v12.22
- 서버 프레임워크: express v4.17
- 데이터베이스 및 라이브러리: sqlite, better-sqlite3 v7.4
- 테스트 프레임워크: jest v27, supertest v6

 

문서의 [하단](#library)에 데이터베이스 라이브러리 및 세션 사용법을 적어두었습니다.

 

---

 

## 요건 1. API 응답 포멧

 

정상처리 및 오류처리에 대한 API 서버 공통 응답 포맷을 아래와 같이 정의합니다.

 

- 정상처리 및 오류처리 모두 success 필드를 포함합니다.
- 정상처리라면 true, 오류처리라면 false 값을 출력합니다.
- 정상처리는 response 필드를 포함하고 error 필드는 null 입니다.
- 응답 데이터가 단일 객체라면, response 필드는 JSON Object로 표현됩니다.
- 응답 데이터가 스칼라 타입(string, number, boolean)이라면, response 필드는 string, number, boolean로 표현됩니다.
- 응답 데이터가 Array라면, response 필드는 JSON Array로 표현됩니다.
- 오류처리는 error 필드를 포함하고 response 필드는 null 입니다. error 필드는 status, message 필드를 포함합니다.
- status : HTTP Response status code 값과 동일한 값을 출력해야 합니다.
- message : 오류 메시지가 출력됩니다.

 

### 1.1. 로그인 성공 응답 예시

 

```json
{
{
"success": true,
"response": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9...이하생략...",
"user": {
"name": "tester",
...
},
},
"error": null
}
```

 

### 1.2. 로그인 실패 응답 예시

 

- 로그인 이메일 누락 (HTTP STATUS 400)

 

```json
{
"success": false,
"response": null,
"error": {
"status": 400,
"message": "이메일은 필수입니다."
}
}
```

 

- 로그인 이메일/비밀번호 미일치 (HTTP STATUS 401)

 

```json
{
"success": false,
"response": null,
"error": {
"status": 401,
"message": "이메일 또는 비밀번호가 다릅니다."
}
}
```

 

## 요건 2. 공개용 API 및 인증 사용자용 API 구분

 

API는 사용자가 로그인하지 않아도 호출할 수 있는 `공개용 API`와 로그인 후 호출할 수 있는 `로그인 사용자용 API` 그리고 참가자 인증 후 호출할 수 있는 `참가자용 API`로 구분됩니다.

 

- 공개용 API는 이미 작성이 되어 있으니 개발시 참고해 주세요.
- 로그인 사용자 인증 정보로 `참가자용 API`를 호출할 수 없습니다. 참가자 인증 정보도 `로그인 사용자용 API`를 호출할 수 없습니다.
- 인증 사용자용 API를 호출하기 위해 요청 헤더에 `Authorization` 항목을 추가하고, 값으로 로그인 후 전달받은 `token``Bearer` 키워드를 앞에 붙여 입력합니다.

 

```bash
curl --request GET 'http://localhost:8080/api/users/me' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9...이하생략...'
```

 

인증 사용자용 API 호출시 `Authorization` 헤더가 누락되거나 값이 올바르지 않다면 아래와 같은 오류 응답이 발생해야 합니다.

 

```json
{
"success": false,
"response": null,
"error": {
"status": 401,
"message": "인증 자격 증명이 유효하지 않습니다."
}
}
```

 

### API

 

- 공개용 API
- 가입: POST /api/users/signup
- 로그인: POST /api/users/login
- 참가자 인증: POST /api/participant/token
- 로그인 사용자용 API
- 문서 생성 POST /api/documents
- 문서 읽기 GET /api/documents/{documentId}
- 문서 삭제 DELETE /api/documents/{documentId}
- 문서 발행 POST /api/documents/{documentId}/publish
- 문서 목록 GET /api/documents
- 참가자용 API
- 참가자 문서 읽기 GET /api/participant/document
- 참가자 싸인 POST /api/participant/sign

 

## 요건 3. API 구현

 

- `state-machine`을 활용해서 문서와 참가자의 상태를 처리해 주세요.
- 계약과정에서 일어나는 액션에 대한 히스토리를 저장해 주세요.
- schema.sql에서 `document_histories``participant_histories` 테이블을 참고해 주세요.
- JSON 데이터 타입은 `JSON.stringify()`로 넣어야 합니다.
- `data`에는 새로 생성되거나 변경되는 정보만 넣어주세요.
- `type`은 API 명세를 확인해 주세요.

 

### 3.1. 문서 생성

 

> `DocumentController.create` 메소드를 구현하세요.

 

문서를 생성합니다.

 

- 로그인 사용자만 호출할 수 있습니다.
- 제목과 내용은 필수입니다.
- 참가자 이름과 이메일은 필수입니다.
- 참가자 이메일은 이메일 형식이어야 합니다.
- 참가자는 최소 2명 최대 10명까지 등록이 가능합니다.
- 문서 ID는 `uuid`를 이용해서 생성합니다. (`UserService.signUp` 메소드를 참고해 주세요.)
- 문서 상태는 `CREATED`로 저장합니다.
- 참가자 목록을 문서 ID와 함께 참가자 ID를 UUID로 생성해서 `participants` 테이블에 `CREATED` 상태로 저장합니다.
- 문서 히스토리와 참가자들 히스토리 타입을 `CREATE`로 저장합니다.

 

| method | path |
| ------ | -------------- |
| POST | /api/documents |

 

Request Body:

 

```json
{
"title": "계약서",
"content": "매우 긴 내용",
"participants": [
{
"name": "참가자",
"email": "email@example.com",
},
...
]
}
```

 

Response Body:

 

```json
{
"success": true,
"response": {
"documentId": "05a05180-c6bb-11eb-b8bc-0242ac130003"
},
"error": null
}
```

 

Exception:

 

- 인증 정보가 없는 경우 (401)
- 제목 또는 내용이 없는 경우 (400)
- 참가자의 이름 또는 이메일이 없는 경우 (400)
- 참가자의 이메일 값이 이메일 형식이 아닌 경우 (400)
- 참가자의 이메일이 중복으로 들어가 있는 경우 (400)
- 참가자가 2명 미만이거나 10명을 초과한 경우 (400)

 

### 3.2. 문서 읽기

 

> `DocumentController.read` 메소드를 구현하세요.

 

문서를 DB에서 읽어서 리턴합니다.

 

- 로그인 사용자이고 문서의 소유자만 호출할 수 있습니다.
- 참가자들의 서명은 응답에 포함하지 않도록 해주세요.

 

| Method | URL |
| ------ | --------------------------- |
| GET | /api/documents/{documentId} |

 

Param:

 

- documentId: 문서 ID

 

Response Body:

 

```json
{
"success": true,
"response": {
"document": {
"id": "05a05180-c6bb-11eb-b8bc-0242ac130003",
"title": "계약서",
"content": "매우 긴 내용",
"status": "PUBLISHED",
"participants": [
{
"id": "b24aee27-1c6c-4294-a4fa-49cf11ea442f",
"name": "참가자",
"email": "email@example.com",
"status": "INVITED",
"createdAt": "2021-06-10T10:00:00.000Z",
"updatedAt": "2021-06-11T10:00:00.000Z"
}
],
"createdAt": "2021-06-10T10:00:00.000Z",
"updatedAt": "2021-06-11T10:00:00.000Z"
}
},
"error": null
}
```

 

Exception:

 

- 인증 정보가 없는 경우 (401)
- 문서의 소유자가 아닌 경우 (403)
- 문서 ID가 올바르지 않은 경우 (404)
- 문서를 찾을 수 없는 경우 (404)

 

### 3.3. 문서 삭제

 

> `DocumentController.remove` 메소드를 구현하세요.

 

문서를 삭제합니다.

 

- 로그인 사용자이고 문서의 소유자만 호출할 수 있습니다.
- 문서의 상태가 `CREATED`인 경우에만 삭제할 수 있습니다.
- 문서와 참가자들의 상태를 `DELETED`로 업데이트하는 논리 삭제를 합니다.
- 이미 삭제 상태인 경우 DB 업데이트를 하지 않고 성공 처리합니다.
- 문서 히스토리와 참가자들 히스토리 타입을 `DELETE`로 저장합니다.

 

| Method | URL |
| ------ | --------------------------- |
| DELETE | /api/documents/{documentId} |

 

Param:

 

- documentId: 문서 ID

 

Response Body:

 

```json
{
"success": true,
"response": true,
"error": null
}
```

 

Exception:

 

- 인증 정보가 없는 경우 (401)
- 문서의 소유자가 아닌 경우 (403)
- 문서 ID가 올바르지 않은 경우 (404)
- 문서를 찾을 수 없는 경우 (404)
- 문서의 상태가 `CREATED`가 아닌 경우 (400)

 

### 3.4. 문서 발행

 

> `DocumentController.publish` 메소드를 구현하세요.

 

참가자들에게 문서를 발행합니다.

 

- 로그인 사용자이고 문서의 소유자만 호출할 수 있습니다.
- 문서가 `CREATED` 상태의 경우에만 발행할 수 있습니다.
- 문서의 상태를 `PUBLISHED`로 업데이트합니다.
- 문서 히스토리 타입을 `PUBLISH`로 저장합니다.
- 문서 참가자들의 상태를 `INVITED` 상태로 업데이트합니다.
- 참가자들의 히스토리 타입을 `INVITED`로 저장합니다.

 

| method | path |
| ------ | ----------------------------------- |
| POST | /api/documents/{documentId}/publish |

 

Param:

 

- documentId: 문서 ID

 

Response Body:

 

```json
{
"success": true,
"response": true,
"error": null
}
```

 

Exception:

 

- 인증 정보가 없는 경우 (401)
- 문서 ID가 올바르지 않은 경우 (404)
- 문서를 찾을 수 없는 경우 (404)
- 문서의 소유자가 아닌 경우 (403)
- 문서의 상태가 `CREATED`가 아닌 경우 (400)

 

### 3.5. 문서 목록 조회

 

> `DocumentController.getAll` 메소드를 구현하세요.

 

문서 목록을 조회합니다.

 

- 로그인 사용자가 호출할 수 있습니다.

 

| Method | URL |
| ------ | --------------------------------- |
| GET | /api/documents?offset&size&status |

 

Query:

 

- offset: 페이징 처리 파라미터 (최솟값: 0, 최댓값: Number.MAX_SAFE_INTEGER 기본값: 0)
- 최솟값-최댓값 범위를 벗어나거나 값이 넘어오지 않았다면, 기본값으로 대체합니다.
- size: 출력할 아이템 개수 (최솟값: 1, 최댓값: 5, 기본값: 5)
- 최솟값-최댓값 범위를 벗어나거나 값이 넘어오지 않았다면, 기본값으로 대체합니다.
- status: 문서 상태 (기본값: none)

 

Response Body:

 

```json
{
"success": true,
"response": {
"documents": [{
"id": "05a05180-c6bb-11eb-b8bc-0242ac130003",
"title": "계약서",
"content": "매우 긴 내용",
"status": "CREATED",
"participants": [{
"id": "b24aee27-1c6c-4294-a4fa-49cf11ea442f",
"name": "참가자",
"email": "email@example.com",
"status": "CREATED",
"createdAt": "2021-06-10T10:00:00.000Z",
"updatedAt": "2021-06-10T10:00:00.000Z",
}],
"createdAt": "2021-06-10T10:00:00.000Z",
"updatedAt": "2021-06-10T10:00:00.000Z",
}, {
...
}]
},
"error": null
}
```

 

Exception:

 

- 인증 정보가 없는 경우 (401)

 

### 3.6. 참가자 문서 읽기

 

> `ParticipantController.readDocument` 메소드를 구현하세요.

 

DB에서 문서를 읽어서 리턴합니다.

 

- 참가자 인증 사용자만 호출할 수 있습니다.
- 참가자 히스토리 타입을 `READ_DOCUMENT`로 저장합니다.

 

| Method | URL |
| ------ | ------------------------- |
| GET | /api/participant/document |

 

Response Body:

 

```json
{
"success": true,
"response": {
"document": {
"id": "05a05180-c6bb-11eb-b8bc-0242ac130003",
"title": "계약서",
"content": "매우 긴 내용",
"status": "PUBLISHED",
"createdAt": "2021-06-10T10:00:00.000Z",
"updatedAt": "2021-06-11T10:00:00.000Z"
}
},
"error": null
}
```

 

Exception:

 

- 참가자 인증 정보가 없는 경우 (401)

 

### 3.7. 참가자 문서 서명

 

> `ParticipantController.sign` 메소드를 구현하세요.

 

참가자가 문서에 서명합니다.

 

- 참가자 인증 사용자만 호출할 수 있습니다.
- 서명은 필수입니다.
- 참가자 서명을 저장하면서 상태를 `SIGNED`로 업데이트합니다.
- 참가자 히스토리 타입을 `SIGN`로 저장합니다.

 

| Method | URL |
| ------ | --------------------- |
| POST | /api/participant/sign |

 

Request Body:

 

```json
{
"signature": "sign"
}
```

 

Response Body:

 

```json
{
"success": true,
"response": true,
"error": null
}
```

 

Exception:

 

- 참가자 인증 정보가 없는 경우 (401)
- 서명이 없는 경우 (400)
- 이미 서명한 문서인 경우 (400)

 

## 요건 4. 보안

 

문서 정보가 노출되면 안 되기 때문에 보안에 각별히 신경을 써야 합니다.

 

### 4.1. 사이트 간 요청 위조 (Cross-site request forgery, CSRF, XSRF) 차단

 

> `src/middlewares/csrf.middleware`를 구현해주세요.

 

`express-session``csrf` 라이브러리가 이미 설치되어 있습니다. 해당 라이브러리를 이용해서 csrf 미들웨어를 작성해 주세요.

 

`csrf` 라이브러리 문서는 [여기](https://github.com/pillarjs/csrf/blob/master/README.md)에서 확인할 수 있습니다.

 

- 요청이 들어올 때마다 token을 생성해서 응답 해더 `x-csrf-token`로 내려주세요.
- `/api` path로 요청이 오면, secret을 생성해서 세션에 저장해 주세요.
- `/api` path가 아닌 요청이 오면, `x-csrf-token` 해더값을 검증해 주세요.
- 마지막 token을 세션에 저장하고 해더값을 검증할 때 토큰 값이 같은지 비교해 주세요.

 

Exception:

 

- 세션에 csrfSecret 정보가 없는 경우 (401)
- 해더 값이 세션에 저장된 csrf token과 같지 않은 경우 (403)
- csrf 토큰 정보가 유효하지 않은 경우 (403)

 

### 4.2. 중복 로그인 방지

 

> `UserController.login``ParticipantController.token` 메소드에 코드를 추가해 주세요.

 

새로운 인증 토큰을 발행할 때 다른 인증 토큰을 만료시켜서 하나의 인증 토큰만 유효하도록 해주세요.

 

- 인증 토큰을 발행 후 세션에 인증 정보를 저장합니다.
- `req.sessionStore`에서 해당 인증의 다른 세션을 찾아서 제거합니다. (참가자ID 또는 사용자ID로 조회)

 

Exception:

 

- 세션 정보가 없을 경우 (401)
- 세션 정보와 인증 정보가 다른 경우 (403)

 

<a id="library"></a>

 

## 라이브러리 설명

 

### better-sqlite3

 

sqlite3 데이터베이스 라이브러리로 비동기 처리를 하지 않아도 됩니다.

 

문서는 [여기](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md)에서 확인할 수 있습니다.

 

기본 동작 사용법은 다음과 같습니다.

 

```js
const Database = require("better-sqlite3");
const db = new Database(":memory:", { verbose: console.log });

 

// .prepare(string) -> Statement
const stmt = db.prepare("SELECT name, age FROM cats");

 

// .all([...bindParameters]) -> array of rows
const cats = stmt.all();

 

// .get([...bindParameters]) -> row
const cat = db.prepare("SELECT name, age FROM cats WHERE id = ?").get(1);

 

// .transaction(function) -> function
const insert = db.prepare("INSERT INTO cats (name, age) VALUES (@name, @age)");

 

const insertMany = db.transaction((cats) => {
for (const cat of cats) insert.run(cat);
});

 

insertMany([
{ name: "Joey", age: 2 },
{ name: "Sally", age: 4 },
{ name: "Junior", age: 1 },
]);

 

// .run([...bindParameters]) -> object
const stmt = db.prepare("INSERT INTO cats (name, age) VALUES (?, ?)");
const info = stmt.run("Joey", 2);

 

console.log(info.changes); // => 1
console.log(info.lastInsertRowid); // => 1

 

// Binding Parameters
const stmt = db.prepare(
"INSERT INTO people VALUES (@firstName, @lastName, @age)"
);
const stmt = db.prepare(
"INSERT INTO people VALUES (:firstName, :lastName, :age)"
);
const stmt = db.prepare(
"INSERT INTO people VALUES ($firstName, $lastName, $age)"
);
const stmt = db.prepare(
"INSERT INTO people VALUES (@firstName, :lastName, $age)"
);

 

stmt.run({
firstName: "John",
lastName: "Smith",
age: 45,
});
```

 

### express-session

 

cookie에 세션 ID를 저장하고 세션을 관리하는 미들웨어 입니다.

 

세션 데이터 저장소로 MemoryStore를 사용합니다.

 

문서는 [여기](https://github.com/expressjs/session/blob/master/README.md)에서 확인할 수 있습니다.

 

여기에서는 간단하게 Session과 SessionStore 사용법에 대해서만 추가했습니다.

 

```js
// access session id
req.sessionID; // or req.session.id

 

// access session data
if (req.session.viewCount) {
req.session.viewCount++;
} else {
req.session.viewCount = 1;
}

 

// session store
const store = req.sessionStore;

 

// Get all sessions in the store as an array.
store.all((err, sessions) => {
console.log(sessions); // { sid: { viewCount: 1 } }
});

 

// Destroys the session
store.destroy("sid", (err) => {});
```

 

 

프로젝트 구조

src 폴더 안 디렉터리를 보면 다음과 같습니다. 

types : 데이터 객체의 타입에 대한 정의

api : api 요청에 대한 응답을 제공

common : 공통 exceptions와 interfaces가 정의

lib : database(sqlite-better3) 연결과 json web token 처리

middlewares : 인증, csrf, error 처리를 위한 middleware

app.ts : 라우팅 처리와 서버 기동 부분

server.ts : 서버의 시작(starting point)

config.ts : 설정 파일

 

api 폴더를 뜯어보면 다음과 같습니다.

세 개의 API 라우팅으로 나뉩니다. documents(문서 처리), participant(참여자 처리), users(사용자 처리)

각 서비스는 dto, entities, controller, service, repository, spec.ts(testing)의 아키텍처로 구성되어 있습니다.

dto : Data Transfer Object  데이터 전송 객체. 각 서비스에서 controller와 service 가 통신할 때 주고받는 데이터의 타입을 정의

entities : 데이터베이스와 직접적으로 통신하는 데이터의 타입에 대한 정의

controller : DTO로 변환된 요청 body를 매개변수로 적절한 Service의 메서드를 호출

- 클라이언트의 요청(Request)을 수신합니다.

- 요청(Request)에 들어온 데이터 및 내용을 검증합니다.

- 서버에서 수행된 결과를 클라이언트에게 반환(Response)합니다.

- 즉, 컨트롤러는 클라이언트의 요청(Request)을 서비스 계층으로 데이터를 전달

Service : DTO를 비즈니스 로직을 통해 Entity로 변환된 객체를 repository를 이용해서 저장.

아키텍처의 가장 핵심적인 비즈니스 로직을 수행하고 실제 사용자(클라이언트)가 원하는 요구사항을 구현한다.

repository : 데이터베이스와 직접 연결. query가 작성되어 있음.

 

https://enjoyk.tistory.com/4#google_vignette

 

이를 계층형 아키텍처라 합니다. 계층형 아키텍쳐는 각 계층은 자신이 맡은 역할만을 수행하며 다른 계층으로부터 분리되어 동작합니다. 

하지만 상위 계층에서 하위 계층으로 로직이 흘러감에 따라 상위 계층은 하위 계층의 속성 및 객체에 접근이 가능한 데에 비해 하위 객체는 그렇지 못합니다. 하지만 개발을 하다 보면 하위 계층에서 상위 계층의 객체에 접근이 불가피하게 되기도 합니다. 그러다 보니 상위 계층을 한 단계 내려 하위 계층에 위치하게 되면 서로 참조를 할 수 있어 제약점은 해결할 수 있지만 하위 계층이 비대해져 좋은 아키텍처가 되지 못합니다. 

 

관련 개념.

OOP : Object Oriented Programming

TDD : Test Driven Development

 

소스 파헤치기

프로세스가 어떻게 흘러가는지 순서도를 그려보면 아래와 같습니다.

초기 프로젝트 소스를 보면 document api 기능은 구현되어 있지 않아 이를 구현하는 게 요건인데 participant와 user와 같은 구조를 따르도록 구현을 해야 합니다. 

 

app.ts

 

class App {
  private app: express.Application;

  constructor(controllers: Controller[]) {
    this.app = express();

    this.initializeMiddlewares();
    this.initializeControllers(controllers);
    this.initializeErrorHandling();
  }

  public listen() {
    const port = process.env.PORT ?? 3000;
    this.app.listen(port, () => {
      console.log(`App listening on the port ${port}`);
    });
  }

  public getServer() {
    return this.app;
  }

  private initializeMiddlewares() {
    this.app.use(express.json());
    this.app.use(
      session({
        name: 'prgrms.sid',
        secret: 'keyboard cat',
        resave: false,
        saveUninitialized: true,
      }),
    );
    this.app.use(csrf());
    this.app.use(verifyJWT);
  }

  private initializeErrorHandling() {
    this.app.use(errorMiddleware);
  }

  private initializeControllers(controllers: Controller[]) {
    const router = Router();

    router.get('/', (req, res) => res.send('OK'));

    controllers.forEach((controller) => {
      router.use(controller.router);
    });

    this.app.use('/api', router);
  }
}

*controllers 객체를 server.ts 로부터 매개변수로 받아 json, jwt, csrf, controller, error-middleware를 순차적으로 실행합니다.

서비스의 진입단은 이렇고 요건 중 이미 구현되어 있는 공개용 API 중 sign 과정을 들여다봅시다.

 

Part of user.controller.ts

 

export default class UserController implements Controller {
  path = "/users";
  router = Router();

  userService = new UserService(new UserRepository()); //user.service.ts의 UserService 클래스

  constructor() {
    this.initializeRoutes();
  }

  initializeRoutes() {
    const router = Router();

    router
      .post("/signup", wrap(this.signUp))
      .post("/login", wrap(this.login))
      .get("/me", wrap(this.me));

    this.router.use(this.path, router);
  }

  signUp: Handler = async (req): Promise<SignUpResponse> => {
    const { email, password, name } = req.body as SignUpDto;

    if (!email) {
      throw new BadRequestException("이메일은 필수입니다.");
    }

    if (!password) {
      throw new BadRequestException("비밀번호는 필수입니다.");
    } else if (password.length < 8) {
      throw new BadRequestException("비밀번호는 최소 8글자 이상입니다.");
    }

    if (!name) {
      throw new BadRequestException("이름은 필수입니다.");
    }

    const { count: hasEmail } = this.userService.countByEmail(email);
    if (hasEmail) {
      throw new BadRequestException("이미 가입된 이메일입니다.");
    }

    await this.userService.signUp({ //실제 로그인 프로세스는 user.service.ts 에 넘긴다.
      email,
      password,
      name,
    });

    return true;
  };
 }

*/signup, /login, /me 는 다 wrap이라는 메서드로 감싸져 있습니다.

lib/request-handler.ts의 wrap은 다음과 같이 생겼습니다.

export const wrap = (handler: Handler) =>
  async (req: Request, res: Response, next: NextFunction) => { 
     try {
       const response = await handler(req, res, next);
       res.json(response);
       next();
     } catch (err) {
        next(err);
     }
};

singup 함수의 return 값을 받아 response(응답)을 전송하고 다음 프로세스를 진행하는 미들웨어 역할을 하는 함수입니다.

user.controller.ts 는 클라이언트로부터 받은 데이터를 검증하고 실제 프로세스 처리는 user.service.ts 에 넘깁니다.

회원가입 절차이니 이메일과 비밀번호 등의 입력값에 대한 검증 및 기존 유저인지 체크를 합니다.

 

Part of user.service.ts

async signUp({ name, email, password }: SignUpDto): Promise<UUID> {
    const { count: hasEmail } = this.countByEmail(email);
    if (hasEmail) {
      throw new BadRequestException("중복된 이메일이 있습니다.");
    }

    const encreyptedPassword = await hash(password, hashRounds);

    const id: UUID = uuidv4();
    const now = new Date().toISOString();

    this.userRepository.create({ //user.repository.ts 에서 데이터베이스에 삽입
      id,
      email,
      name,
      password: encreyptedPassword,
      created_at: now,
      updated_at: now,
    });

    return id;
  }

*특이점이 있다면 앞서 user.controller.ts 에서 중복이메일 체크를 했는데 user.service.ts 에서 한번 더 한다는 겁니다. 굳이 user.service.ts 에선 없어도 될 것 같은데 말이죠..

아무튼 user.service.ts 에선 controller로부터 받은 데이터(패스워드)를 알맞게 변경한 다음(암호화) 데이터베이스와 통신을 담당하는 user.repository.ts 에 데이터를 전달합니다.

 

Part of user.repository.ts

 

create(raw: UserRaw) {
    const result = db
      .prepare(
        [
          "INSERT INTO",
          this.tableName,
          "(id, email, name, password, created_at, updated_at)",
          "VALUES",
          "($id, $email, $name, $password, $created_at, $updated_at)",
        ].join(" ")
      )
      .run(raw);

    return result;
  }

직접 데이터베이스에 INSERT문을 전송합니다. 


다음글에서 과제에서 주어진 요건을 보면서 한 문제씩 해결해 보도록 합시다.