Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/litsynp/triple-club-mileage-service

🧳 νŠΈλ¦¬ν”Œμ—¬ν–‰μž 클럽 λ§ˆμΌλ¦¬μ§€ μ„œλΉ„μŠ€
https://github.com/litsynp/triple-club-mileage-service

docker mysql querydsl-jpa spring-boot spring-data-jpa spring-rest-docs

Last synced: about 10 hours ago
JSON representation

🧳 νŠΈλ¦¬ν”Œμ—¬ν–‰μž 클럽 λ§ˆμΌλ¦¬μ§€ μ„œλΉ„μŠ€

Awesome Lists containing this project

README

        

# 🧳 νŠΈλ¦¬ν”Œμ—¬ν–‰μž 클럽 λ§ˆμΌλ¦¬μ§€ μ„œλΉ„μŠ€

νŠΈλ¦¬ν”Œ μ‚¬μš©μžλ“€μ΄ μž₯μ†Œμ— 리뷰λ₯Ό μž‘μ„±ν•  λ•Œ 포인트λ₯Ό λΆ€μ—¬ν•˜κ³ , 전체/κ°œμΈμ— λŒ€ν•œ 포인트 λΆ€μ—¬ νžˆμŠ€ν† λ¦¬μ™€, κ°œμΈλ³„ λˆ„μ  포인트λ₯Ό κ΄€λ¦¬ν•˜κ³ μž ν•©λ‹ˆλ‹€.

Spring Boot μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ΄μš©ν•˜μ—¬ 주어진 문제λ₯Ό ν•΄κ²°ν•˜κ³  κ·Έ λ°©μ•ˆμ— λŒ€ν•΄μ„œ μ΄μ•ΌκΈ°ν•©λ‹ˆλ‹€.

## πŸ” κ΅¬ν˜„ κΈ°λŠ₯

- μž₯μ†Œμ— 리뷰λ₯Ό μž‘μ„±ν•˜λ©΄ 포인트λ₯Ό λΆ€μ—¬
- 전체/κ°œμΈμ— λŒ€ν•œ 포인트 λΆ€μ—¬ νžˆμŠ€ν† λ¦¬ 관리
- κ°œμΈλ³„ λˆ„μ  포인트 관리

## βœ… μš”κ΅¬μ‚¬ν•­ 및 체크리슀트

- [x] MySQL β‰₯ 5.7 μ‚¬μš©
- [x] ν…Œμ΄λΈ”κ³Ό μΈλ±μŠ€μ— λŒ€ν•œ DDL μž‘μ„±
- [x] μ• ν”Œλ¦¬μΌ€μ΄μ…˜μœΌλ‘œ λ‹€μŒ API 제곡
- [x] `POST /events` 둜 ν˜ΈμΆœν•˜λŠ” 포인트 적립 API
- [x] 포인트 쑰회 API
- 상세 μš”κ΅¬μ‚¬ν•­
- [x] REST APIλ₯Ό μ œκ³΅ν•˜λŠ” μ„œλ²„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κ΅¬ν˜„
- [x] Java, Kotlin, Python, JavaScript(or TypeScript) 쀑 μ–Έμ–΄ 선택
- [x] Framework, Library 자유 μ‚¬μš©, μΆ”κ°€ Data Storage ν•„μš”μ‹œ μ—¬λŸ¬ μ’…λ₯˜ μ‚¬μš© κ°€λŠ₯
- [x] README μž‘μ„±
- [x] ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€ μž‘μ„± (Optional)

## ❓ μ‚¬μš© 방법

λ¨Όμ € `docker compose`둜 MySQL μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€. ([`docker-compose.yml λ‚΄μš©`](https://github.com/litsynp/triple-club-mileage-service/blob/main/docker-compose.yml))

```bash
$ docker compose up
```

μ»¨ν…Œμ΄λ„ˆκ°€ 쀀비됐닀면 μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λΉŒλ“œ ν›„ μ‹€ν–‰ν•©λ‹ˆλ‹€.

```bash
$ ./gradlew build && java -jar build/libs/mileage-service-0.0.1-SNAPSHOT.jar
```

μ„œλ²„κ°€ λ„μ›Œμ‘Œλ‹€λ©΄

- **리뷰 μž‘μ„± 이벀트 API**인 `POST http://localhost:8080/events`
```json
{
"type": "REVIEW",
"action": "ADD",
"reviewId": "ff35e929-fcf6-11ec-b3c2-0242ac170002",
"userId": "31313130-3031-3131-3130-000000000000",
"placeId": "8040a09f-fcf6-11ec-b3c2-0242ac170002",
"content": "μ’‹μ•„μš”!",
"attachedPhotoIds": ["48925641-70f3-4674-86e6-420bbab59bf8", "cf00ec57-563b-4f0e-b5bf-78ce28738efb"]
}
```

- **μ‚¬μš©μž 포인트 총점 쑰회 API**인 `GET http://localhost:8080/points?user-id=31313130-3031-3131-3130-000000000000`

- νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ μ œκ³΅ν•˜λŠ” **μ‚¬μš©μž 포인트 기둝 쑰회 API**인 `GET http://localhost:8080/point-histories`

와 같이, API λͺ…세에 맞게 싀행해보싀 수 μžˆμŠ΅λ‹ˆλ‹€.

- 미리 **μ‚¬μš©μž 2λͺ…, μž₯μ†Œ 1개, 사진 2개**λ₯Ό [`src/main/resources/data.sql`](https://github.com/litsynp/triple-club-mileage-service/blob/main/src/main/resources/data.sql)에 λ„£μ–΄μ„œ 진행할 수 μžˆλ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€. ν•„μš”μ— 따라 SQL을 μ‹€ν–‰ν•˜μ‹œλ©΄ λ©λ‹ˆλ‹€.

## βš™οΈ μ£Όμš” μ‚¬μš© ν”„λ ˆμž„μ›Œν¬ / 라이브러리 및 버전

- Spring Boot 2.7.1
- Querydsl JPA 5.0.0
- Spring REST Docs (API λ¬Έμ„œν™”)
- p6spy (SQL logging)
- Java 11
- Gradle 7.4.1
- MySQL (8.0.29) (InnoDB)
- Docker & docker compose (MySQL μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰)

## πŸš— ν”„λ‘œμ νŠΈ ꡬ쑰 및 μ•„ν‚€ν…μ²˜ μš”μ•½

![3-tier-layered-architecture](https://user-images.githubusercontent.com/42485462/178142905-86592505-b3c5-455f-91de-7f2d38010e29.png)

좜처: https://www.petrikainulainen.net/software-development/design/understanding-spring-web-application-architecture-the-classic-way/

- ν”„λ‘œμ νŠΈ κ΅¬μ‘°λŠ” μœ„μ™€ 같이 3 tier layered architecture둜 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€. Web, Service, Repository둜 κ΅¬λΆ„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

- **ν΄λΌμ΄μ–ΈνŠΈ ↔ Controller**에 μ‚¬μš©λ˜λŠ” DTO와, **Controller ↔ Service**에 μ‚¬μš©λ˜λŠ” DTOλ₯Ό κ΅¬λΆ„ν•˜μ—¬ κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

- API controller ν΄λž˜μŠ€λŠ” [`api`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/api), service ν΄λž˜μŠ€λŠ” [`service`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/service), repositoryλŠ” [`dao`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/dao), entityλŠ” [`domain`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/domain) νŒ¨ν‚€μ§€μ— 관심사에 따라 λͺ¨μ•„λ‘μ—ˆμŠ΅λ‹ˆλ‹€.
- 각 κ³„μΈ΅μ—μ„œ μ‚¬μš©λ˜λŠ” DTOλŠ” [`dto`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/dto) νŒ¨ν‚€μ§€μ— μš©λ„μ— 따라 λͺ¨μ•„λ‘μ—ˆμŠ΅λ‹ˆλ‹€.
- 각 DTOμ—λŠ” `@NotNull`κ³Ό 같은 μ–΄λ…Έν…Œμ΄μ…˜μ„ μ΄μš©ν•œ validation이 μ μš©λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.
- ν†΅μΌλœ μ–‘μ‹μ˜ exception handling을 μœ„ν•΄ [`global.error`](https://github.com/litsynp/triple-club-mileage-service/tree/main/src/main/java/com/litsynp/mileageservice/global/error) νŒ¨ν‚€μ§€μ— exception handler 및 exception, error code 등을 λͺ¨μ•„λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

## πŸ‘₯ SQL Schema 및 ERD

λ‹€μŒμ€ μš”κ΅¬μ‚¬ν•­μ„ λ°”νƒ•μœΌλ‘œ μž‘μ„±ν•œ ERDμž…λ‹ˆλ‹€.

![triple-erd](https://user-images.githubusercontent.com/42485462/178138740-3f335bc5-13f7-4b0f-a634-436d72894e78.png)

μŠ€ν‚€λ§ˆλŠ” [`src/main/resources/schema.sql`](https://github.com/litsynp/triple-club-mileage-service/blob/main/src/main/resources/schema.sql) 에 μž‘μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

DDL Schema μž‘μ„± 및 unique & foreign constraint, index 섀정을 ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

DB Engine은 **InnoDB**둜 μ‚¬μš©ν•©λ‹ˆλ‹€.

## 🧾 API λͺ…μ„Έ

EndpointλŠ” 리뷰 μž‘μ„±, μˆ˜μ •, μ‚­μ œ 이벀트λ₯Ό μ „λ‹¬ν•˜λŠ” `/events`, μ‚¬μš©μž 점수 합계λ₯Ό ν™•μΈν•˜λŠ” `/points`, μ‚¬μš©μž 점수 기둝 λͺ©λ‘μ„ μ‘°νšŒν•˜λŠ” `/point-histories` 둜 총 3κ°œμž…λ‹ˆλ‹€. 이벀트의 μ’…λ₯˜κΉŒμ§€ κ³„μ‚°ν•˜λ©΄ 5개의 APIκ°€ λ©λ‹ˆλ‹€.

API λͺ…μ„ΈλŠ” Spring REST Docs을 μ΄μš©ν•΄ ν…ŒμŠ€νŠΈλ₯Ό 톡해 λ¬Έμ„œν™”ν–ˆμŠ΅λ‹ˆλ‹€.

[http://localhost:8080/docs/index.html](http://localhost:8080/docs/index.html) 둜 μ ‘μ†ν•˜λ©΄ ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.

μΆ”κ°€λ‘œ PDF둜 μ œμž‘ν•˜μ—¬ μ²¨λΆ€ν•©λ‹ˆλ‹€. [트라플 클ᄅα…₯α†Έ α„†α…‘α„‹α…΅α†―α„…α…΅α„Œα…΅ ᄉα…₯바스 API PDF](https://github.com/litsynp/triple-club-mileage-service/blob/main/triple-club-mileage-service-api-spec.pdf)

## πŸ‘ Test κ²°κ³Ό

JUnit 5, Assertj, BDDMockito, Spring REST Docs 및 MockMvc 등을 톡해 μœ λ‹› ν…ŒμŠ€νŠΈ 및 톡합 ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

![image](https://user-images.githubusercontent.com/42485462/178146949-0305612d-a6d0-4969-87fe-08dc696e868b.png)

## 🏁 REMARKS ν•΄κ²° λ°©μ•ˆ

μš”κ΅¬μ‚¬ν•­ λ¬Έμ„œμ˜ Remarks에 적힌 각 λ¬Έμ œμ— λŒ€ν•œ ν•΄κ²° λ°©μ•ˆμž…λ‹ˆλ‹€.

### βœ…οΈ ν•œ μ‚¬μš©μžλŠ” μž₯μ†Œλ§ˆλ‹€ 리뷰λ₯Ό 1개만 μž‘μ„±ν•  수 있고, λ¦¬λ·°λŠ” μˆ˜μ • λ˜λŠ” μ‚­μ œν•  수 μžˆλ‹€.

ν•œ μ‚¬μš©μžκ°€ μž₯μ†Œλ§ˆλ‹€ 리뷰λ₯Ό 1개만 μž‘μ„±ν•  수 μžˆλŠ” 쑰건은 미리 DB에 unique constraintλ₯Ό κ±Έμ—ˆμŠ΅λ‹ˆλ‹€.

```sql
alter table review
add unique review_ak01 (user_id, place_id);
```

이후 리뷰 μž‘μ„± 이벀트 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ λ©”μ„œλ“œμΈ `ReviewService.writeReview()` μ—μ„œλ„ ν•΄λ‹Ή μž₯μ†Œμ— λŒ€ν•΄ μ‚¬μš©μžκ°€ μž‘μ„±ν•œ 리뷰가 이미 μ‘΄μž¬ν•˜λŠ”μ§€λ₯Ό ν™•μΈν•˜κ³ , μ‘΄μž¬ν•œλ‹€λ©΄ `409 CONFLICT` λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.

리뷰 μˆ˜μ • λ˜λŠ” μ‚­μ œλŠ” 이벀트 APIμ—μ„œ `ADD` 외에도 `MOD` 및 `DELETE` `action`을 μ§€μ›ν•˜μ—¬ ν•΄κ²°ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

### βœ…οΈ 리뷰 보상 μ μˆ˜κ°€ μ‘΄μž¬ν•œλ‹€.

리뷰 보상 μ μˆ˜λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

```
λ‚΄μš© 점수
- 1자 이상 ν…μŠ€νŠΈ μž‘μ„±: 1점
- 1μž₯ 이상 사진 첨뢀: 1점

λ³΄λ„ˆμŠ€ 점수
- νŠΉμ • μž₯μ†Œμ— 첫 리뷰 μž‘μ„±: 1점
```

μ„œλΉ„μŠ€ λ©”μ„œλ“œμΈ `ReviewService.writeReview()` μ—μ„œ 점수λ₯Ό κ³„μ‚°ν•˜λŠ” κ²ƒμœΌλ‘œ ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€. λ’€μ—μ„œ μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

### βœ… 포인트 증감이 μžˆμ„ λ•Œλ§ˆλ‹€ 이λ ₯이 남아야 ν•œλ‹€.

μž‘μ„±, μˆ˜μ •, μ‚­μ œ Review 이벀트λ₯Ό `POST /events` APIλ₯Ό 톡해 전달할 λ•Œλ§ˆλ‹€ μ‚¬μš©μžμ˜ ν¬μΈνŠΈκ°€ λ³€λ™λ˜λ©°, κ·Έ 기둝은 μ‹œκ°„κ³Ό ν•¨κ»˜ μ–Όλ§ˆλ‚˜ λ³€λ™λλŠ”μ§€ 이λ ₯이 λ‚¨μŠ΅λ‹ˆλ‹€.

μš”μ²­μœΌλ‘œ μ „λ‹¬λœ DTOλ₯Ό 읽어 μ–΄λ–€ μœ ν˜•μ˜ μ΄λ²€νŠΈμΈμ§€ νŒλ‹¨ν•˜μ—¬, `ReviewService` ν΄λž˜μŠ€μ— μ •μ˜λœ `writeReview`, `updateReview`, `deleteReviewById` λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ©λ‹ˆλ‹€.

### βœ… μ‚¬μš©μžλ§ˆλ‹€ ν˜„μž¬ μ‹œμ μ˜ 포인트 총점을 μ‘°νšŒν•˜κ±°λ‚˜ 계산할 수 μžˆμ–΄μ•Ό ν•œλ‹€.

각 μ‚¬μš©μžμ˜ 포인트의 총점은 `GET /points` API둜 μ‘°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. Request parameter둜 UUID인 μ‚¬μš©μž IDλ₯Ό 전달해야 ν•˜λ©° (예: `GET /points?user-id=…`), 포인트의 총점을 λ‹€μŒκ³Ό 같은 ν˜•μ‹μœΌλ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€.

```json
{
"userId": "<μ‚¬μš©μž ID>",
"points": 4
}
```

### βœ… 포인트 λΆ€μ—¬ API κ΅¬ν˜„μ— ν•„μš”ν•œ SQL μˆ˜ν–‰ μ‹œ, 전체 ν…Œμ΄λΈ” μŠ€μΊ”μ΄ μΌμ–΄λ‚˜μ§€ μ•ŠλŠ” μΈλ±μŠ€κ°€ ν•„μš”ν•˜λ‹€.

MySQLμ—μ„œ InnoDBλ₯Ό μ‚¬μš©ν•˜μ˜€μœΌλ©°, μ™Έλž˜ν‚€ 섀정을 ν•˜λ©΄ 인덱슀 섀정이 λ©λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ μΆ”κ°€λ‘œ μ™Έλž˜ν‚€μ—λ„ 인덱슀 섀정을 ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ 섀정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

- μ‚¬μš©μž 점수 ν…Œμ΄λΈ”μΈ `user_point` 에 μ‚¬μš©μž 점수 변동 기둝 `100`개λ₯Ό λ„£μ—ˆμŠ΅λ‹ˆλ‹€.

- 100개 μ€‘μ—μ„œ 1개의 기둝만이 μ‚¬μš©μžκ°€ λ³΄μœ ν•œ 점수 κΈ°λ‘μž…λ‹ˆλ‹€.

- `schema.sql` νŒŒμΌμ— μ •μ˜λœ ν…Œμ΄λΈ” DDLμ—μ„œ ν…Œμ΄λΈ”μ„ 생성할 λ•Œ `engine = InnoDB` 둜 **InnoDB**둜 μ„€μ •ν•΄λ‘μ—ˆμŠ΅λ‹ˆλ‹€.

- μœ„μ—μ„œ λ§μ”€λ“œλ Έλ‹€μ‹œν”Ό, MySQL InnoDBλŠ” μ™Έλž˜ν‚€μ— λŒ€ν•΄μ„œλŠ” indexκ°€ μžλ™μœΌλ‘œ μ μš©λ©λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ foreign key constraint 말고도 index도 μΆ”κ°€λ‘œ κ±Έμ—ˆμŠ΅λ‹ˆλ‹€.

`user_id`에 λŒ€ν•΄μ„œ λ‹€μŒκ³Ό 같이 indexλ₯Ό μ μš©ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

```sql
alter table user_point
add index user_point_ak01 (user_id);
```

그리고 λ‹€μŒ 쿼리 ν”Œλžœμ„ ν™•μΈν•˜κΈ° μœ„ν•΄ queryλ₯Ό μ‹€ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

```sql
explain
select coalesce(sum(amount), 0)
from user_point
where user_point.user_id = @user_id;
```

κ²°κ³ΌλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

![select-sum-user-point-query-plan](https://user-images.githubusercontent.com/42485462/178138819-9bfad8b9-c59d-4aa6-a5e3-3a6a87deb263.png)

`type`은 `ref`, `Using index condition`으둜, 인덱슀λ₯Ό μ‚¬μš©ν•΄ μ‘°νšŒν•˜κ³  있으며, 전체 ν…Œμ΄λΈ” μŠ€μΊ”μ΄ μΌμ–΄λ‚˜μ§€ μ•Šμ€ 것을 μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€.

### βœ… 리뷰λ₯Ό μž‘μ„±ν–ˆλ‹€κ°€ μ‚­μ œν•˜λ©΄ ν•΄λ‹Ή 리뷰둜 λΆ€μ—¬ν•œ λ‚΄μš© μ μˆ˜μ™€ λ³΄λ„ˆμŠ€ μ μˆ˜λŠ” νšŒμˆ˜ν•œλ‹€.

리뷰λ₯Ό μž‘μ„±ν•˜λ©΄ `user_point` ν…Œμ΄λΈ”μ— λ‹€μŒκ³Ό 같은 pseudo code처럼 μ μˆ˜κ°€ μΆ”κ°€λ©λ‹ˆλ‹€.

```python
amount = 0

# 1자 이상 ν…μŠ€νŠΈ μž‘μ„±: 1점
if len(dto.content) > 0:
amount = amount + 1

# 1μž₯ 이상 사진 첨뢀: 1점
if len(dto.attachedPhotoIds):
amount = amount + 1

# 첫 리뷰 μž‘μ„±: 1점
if not exists(review where review.placeId = dto.placeId):
amount = amount + 1

if amount > 0:
newUserPoint = UserPoint(user, review, amount)
save newUserPoint to user_point table
```

즉, μž‘μ„±μ—μ„œ λ°œμƒν•œ μ μˆ˜κ°€ 1 이상일 λ•Œλ§Œ μ €μž₯ν•©λ‹ˆλ‹€.

`user_point` ν…Œμ΄λΈ”μ€ `review` ν…Œμ΄λΈ”κ³Ό FK둜 μ—°κ²°λœ ν…Œμ΄λΈ”μž…λ‹ˆλ‹€. `ON DELETE SET NULL` μ˜΅μ…˜μœΌλ‘œ μ‚­μ œλ  λ•Œ 리뷰가 μ‚­μ œλ˜λ”λΌλ„ μ‚¬μš©μž 점수 기둝은 μ‚­μ œλ˜μ§€ μ•Šκ³  FKκ°€ `null`이 λ˜λ„λ‘ ν•΄μ„œ 기둝을 μœ μ§€ν•©λ‹ˆλ‹€.

```sql
alter table user_point
add constraint user_point_fk02 foreign key (review_id) references review (id) on delete set null on update cascade;
```

점수λ₯Ό νšŒμˆ˜ν•  λ•Œμ—λŠ” λ‹€μŒκ³Ό 같이 μ§„ν–‰λ©λ‹ˆλ‹€.

```python
# 리뷰λ₯Ό μ‚­μ œν•˜λ©΄ ν•΄λ‹Ή 리뷰둜 λΆ€μ—¬ν•œ λ‚΄μš© μ μˆ˜μ™€ λ³΄λ„ˆμŠ€ 점수 회수
# ν•˜μ§€λ§Œ 기둝 μœ μ§€λ₯Ό μœ„ν•΄ μ‚­μ œν•΄μ„œλŠ” μ•ˆλœλ‹€.
pointsFromReview = getUserPoints(user=review.user, review=review) # e.g.) 3

# ν•΄λ‹Ή λ¦¬λ·°λ‘œλΆ€ν„° 얻은 점수λ₯Ό κ³„μ‚°ν•˜μ—¬ νšŒμˆ˜ν•œλ‹€.
if pointsFromReview > 0L:
# λ¦¬λ·°λ‘œλΆ€ν„° 받은 κ°’λ§ŒνΌ μ°¨κ°ν•˜λ©΄ λœλ‹€
newUserPoint = UserPoint(user, review, amount=-pointsFromReview) # e.g) -3
save newUserPoint to user_point table
```

총점을 κ³„μ‚°ν•΄μ„œ μ‚­μ œν•˜λ―€λ‘œ, 리뷰 μˆ˜μ • λ“±μœΌλ‘œ 회수된 μ μˆ˜κΉŒμ§€ κ³ λ €ν•΄μ„œ μ΅œμ’…μ μœΌλ‘œ νšŒμˆ˜ν•  μ μˆ˜κ°€ κ³„μ‚°λ©λ‹ˆλ‹€.

λ¦¬λ·°λŠ” 이후 μ‚­μ œν•˜κ²Œ λ©λ‹ˆλ‹€.

μ‚­μ œ ν›„, 같은 μž₯μ†Œμ— μ‚¬μš©μžκ°€ 리뷰λ₯Ό μž‘μ„±ν•˜κ²Œ λœλ‹€λ©΄, `review` ν…Œμ΄λΈ”μ— λ¦¬λ·°λŠ” μ—†κΈ° λ•Œλ¬Έμ— λ³΄λ„ˆμŠ€ 점수 계산이 처음 μž‘μ„±ν•˜λŠ” 것과 λ™μΌν•˜κ²Œ μ§„ν–‰λ©λ‹ˆλ‹€.

### βœ… 리뷰λ₯Ό μˆ˜μ •ν•˜λ©΄ μˆ˜μ •ν•œ λ‚΄μš©μ— λ§žλŠ” λ‚΄μš© 점수λ₯Ό κ³„μ‚°ν•˜μ—¬ 점수λ₯Ό λΆ€μ—¬ν•˜κ±°λ‚˜ νšŒμˆ˜ν•œλ‹€.

μš”κ΅¬μ‚¬ν•­μ— λ”°λ₯΄λ©΄ 리뷰λ₯Ό μˆ˜μ •ν•˜λ©΄ λ‹€μŒκ³Ό 같이 μ§„ν–‰λ©λ‹ˆλ‹€.

```
1. κΈ€λ§Œ μž‘μ„±ν•œ 리뷰에 사진을 μΆ”κ°€ν•˜λ©΄ 1점을 λΆ€μ—¬
2. κΈ€κ³Ό 사진이 μžˆλŠ” λ¦¬λ·°μ—μ„œ 사진을 λͺ¨λ‘ μ‚­μ œν•˜λ©΄ 1점을 회수
```

μ‚¬μ§„λ§Œ 있고 글이 μ—†λŠ” 리뷰가 μ‘΄μž¬ν•  수 μžˆλ‹€κ³  κ°€μ •ν•˜κ³  κ°œλ°œν–ˆμŠ΅λ‹ˆλ‹€.

이 가정에 λ”°λ₯Έλ‹€λ©΄, 1, 2번 쑰건은 λͺ¨λ‘ 글이 μžˆμ–΄μ•Ό ν•œλ‹€λŠ” 것을 μ „μ œλ‘œ ν•˜μ§€λ§Œ, κ·Έλ ‡κ²Œ ν•œλ‹€λ©΄ 2번 쑰건을 μ•…μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

1. **글을 λ¨Όμ € μ‚­μ œ**ν•˜κ³ , **이후 사진을 μ‚­μ œ**ν•˜λ©΄ 2번 쑰건의 `κΈ€κ³Ό 사진이 μžˆλŠ” λ¦¬λ·°μ—μ„œ` λΌλŠ” μ „μ œμ‘°κ±΄μ„ νšŒν”Όν•΄ 회수λ₯Ό νšŒν”Όν•  수 μžˆμŠ΅λ‹ˆλ‹€.
2. 이후 **λ‹€μ‹œ 글을 μž‘μ„±ν•œ** λ’€, **사진을 μΆ”κ°€**ν•˜λ©΄ 1번 쑰건의 `κΈ€λ§Œ μž‘μ„±ν•œ 리뷰에 사진을 μΆ”κ°€` 쑰건이 λ°˜μ˜λ˜μ–΄ 또 1점을 μ–»κ²Œ λ©λ‹ˆλ‹€.

λ”°λΌμ„œ 글이 μžˆλ“  μ—†λ“  사진이 λ³€λ™λœλ‹€λ©΄ μ μˆ˜μ— 변동을 주도둝 ν–ˆμŠ΅λ‹ˆλ‹€.

이 점수 계산은 λ‹€μŒκ³Ό 같이 μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

```
1. 리뷰에 사진이 이전에 μ—†μ—ˆλŠ”μ§€ ν™•μΈν•œλ‹€.
2. 사진이 μ—†μ—ˆλŠ”λ° 사진을 1μž₯ 이상 μΆ”κ°€ν–ˆλ‹€λ©΄ 1점을 λΆ€μ—¬ν•œλ‹€.
3. 사진이 1μž₯ 이상 μžˆμ—ˆλŠ”λ° 사진을 λͺ¨λ‘ μ‚­μ œν–ˆλ‹€λ©΄, ν•΄λ‹Ή 리뷰λ₯Ό 톡해 1점을 λΆ€μ—¬ν•œ 적이 μžˆλ‹€λ©΄ 1점을 μ°¨κ°ν•œλ‹€.
```

리뷰 μˆ˜μ •μ˜ 점수 계산은 λ‹€μŒκ³Ό 같은 pseudo code둜 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

```python
# 이전에 사진이 μžˆμ—ˆλŠ”μ§€ 확인
emptyPhotosBefore = len(review.attachedPhotos) == 0

# κΈ°μ‘΄ 리뷰에 μ €μž₯된 사진 쀑, μƒˆλ‘œ μΆ”κ°€λœ 사진이 μ•„λ‹Œ 사진은 μ „λΆ€ μ‚­μ œ
review.photos.filter(photo.id not in dto.attachedPhotoIds).delete()

# μƒˆλ‘œ μΆ”κ°€λœ 사진 μ €μž₯
review.photos.addAll(dto.attachedPhotoIds)

## 리뷰λ₯Ό μˆ˜μ •ν•˜λ©΄ μˆ˜μ •ν•œ λ‚΄μš©μ— λ§žλŠ” λ‚΄μš© 점수λ₯Ό κ³„μ‚°ν•˜μ—¬ 점수λ₯Ό λΆ€μ—¬ν•˜κ±°λ‚˜ 회수 ##

# κΈ€λ§Œ μž‘μ„±ν•œ 리뷰에 사진을 μΆ”κ°€ν•˜λ©΄ 1점을 μΆ”κ°€
if emptyPhotosBefore and len(review.photos) != 0:
newUserPoint = UserPoint(user, review, amount=1)
save newUserPoint to userPoint table

# κΈ€κ³Ό 사진이 μžˆλŠ” λ¦¬λ·°μ—μ„œ 사진을 λͺ¨λ‘ μ‚­μ œν•˜λ©΄ 1점을 회수
if (not emptyPhotosBefore) and len(review.photos) == 0:
userPoints = getAllUserPoints(user.id)
if (userPoints > 0):
newUserPoint = UserPoint(user, review, amount=-1)
save newUserPoint to userPoint table
```

μœ„μ™€ 같이 글을 올리기 μ „μ˜ μ‚¬μ§„μ˜ 갯수, 글을 올린 ν›„μ˜ μ‚¬μ§„μ˜ 갯수λ₯Ό μ΄μš©ν•˜μ—¬ 점수λ₯Ό κ³„μ‚°ν–ˆμŠ΅λ‹ˆλ‹€.

\* **μš”κ΅¬μ‚¬ν•­μ— λ”°λ₯΄λ©΄ 글을 μž‘μ„±ν•œλ‹€λ©΄ 1점이 μΆ”κ°€λ˜μ§€λ§Œ, 글이 μ—†λŠ” μƒνƒœμ—μ„œ 글을 μΆ”κ°€ν•˜κ±°λ‚˜, 글이 μžˆλŠ” μƒνƒœμ—μ„œ 없도둝 μˆ˜μ •ν•˜λ”λΌλ„ 포인트의 λ³€ν™”λŠ” μΌμ–΄λ‚˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.**

### βœ… μ‚¬μš©μž μž…μž₯μ—μ„œ λ³Έ β€˜μ²« 리뷰'일 λ•Œ λ³΄λ„ˆμŠ€ 점수λ₯Ό λΆ€μ—¬ν•œλ‹€.

```
1. μ–΄λ–€ μž₯μ†Œμ— μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό 남겼닀가 μ‚­μ œν•˜κ³ , μ‚­μ œλœ 이후 μ‚¬μš©μž Bκ°€ 리뷰λ₯Ό 남기면 μ‚¬μš©μž Bμ—κ²Œ λ³΄λ„ˆμŠ€ 점수λ₯Ό λΆ€μ—¬ν•œλ‹€.
2. μ–΄λ–€ μž₯μ†Œμ— μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό 남겼닀가 μ‚­μ œν•˜λŠ”λ°, μ‚­μ œλ˜κΈ° 이전 μ‚¬μš©μž Bκ°€ 리뷰λ₯Ό 남기면 μ‚¬μš©μž Bμ—κ²Œ 점수λ₯Ό λΆ€μ—¬ν•˜μ§€ μ•ŠλŠ”λ‹€.
```

1, 2λ₯Ό λ™μ‹œμ— κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œλŠ” **λ‹¨μˆœνžˆ 리뷰λ₯Ό μ‚­μ œν•˜λ©΄ 리뷰 ν…Œμ΄λΈ”μ—μ„œ μ‚­μ œ**ν•˜λ©΄ λ©λ‹ˆλ‹€. 그리고 **리뷰λ₯Ό μž‘μ„±ν•˜λŠ” μ‹œμ μ—** ν•΄λ‹Ή μž₯μ†Œμ— 리뷰λ₯Ό 남긴 μ‚¬λžŒμ΄ μ—†λŠ”μ§€ ν™•μΈν•˜κ³  점수λ₯Ό κ³„μ‚°ν•˜λ©΄ λ©λ‹ˆλ‹€.

λ”°λΌμ„œ λ‹€μŒκ³Ό 같이 μ§„ν–‰λ©λ‹ˆλ‹€.

```
1. μ–΄λ–€ μž₯μ†Œμ— μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό 남겼닀가 μ‚­μ œν•˜κ³ , μ‚­μ œλœ 이후 μ‚¬μš©μž Bκ°€ 리뷰λ₯Ό 남기면 μ‚¬μš©μž Bμ—κ²Œ λ³΄λ„ˆμŠ€ 점수λ₯Ό λΆ€μ—¬ν•œλ‹€.
1. μ‚¬μš©μž Aκ°€ μž₯μ†Œ P에 리뷰λ₯Ό 남긴닀.
2. μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό μ‚­μ œν•œλ‹€. μ‚¬μš©μž A의 회수 ν¬μΈνŠΈκ°€ κ³„μ‚°λ˜μ–΄ κΈ°λ‘λœλ‹€.
3. μ‚¬μš©μž Bκ°€ μž₯μ†Œ P에 리뷰λ₯Ό 남긴닀. ν•΄λ‹Ή μž₯μ†Œμ— 리뷰가 μ—†μœΌλ―€λ‘œ λ³΄λ„ˆμŠ€ 1점 μΆ”κ°€ν•œλ‹€.

2. μ–΄λ–€ μž₯μ†Œμ— μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό 남겼닀가 μ‚­μ œν•˜λŠ”λ°, μ‚­μ œλ˜κΈ° 이전 μ‚¬μš©μž Bκ°€ 리뷰λ₯Ό 남기면 μ‚¬μš©μž Bμ—κ²Œ 점수λ₯Ό λΆ€μ—¬ν•˜μ§€ μ•ŠλŠ”λ‹€.
1. μ‚¬μš©μž Aκ°€ μž₯μ†Œ P에 리뷰λ₯Ό 남긴닀.
2. μ‚¬μš©μž Bκ°€ μž₯μ†Œ P에 리뷰λ₯Ό 남긴닀. 이미 ν•΄λ‹Ή μž₯μ†Œμ— 리뷰가 μžˆμœΌλ―€λ‘œ λ³΄λ„ˆμŠ€ μ μˆ˜λŠ” μ—†λ‹€.
3. μ‚¬μš©μž Aκ°€ 리뷰λ₯Ό μ‚­μ œν•œλ‹€. μ‚¬μš©μž A의 회수 ν¬μΈνŠΈκ°€ κ³„μ‚°λ˜μ–΄ κΈ°λ‘λœλ‹€.
```

## πŸ€” 회고

### πŸͺ΅ ν…ŒμŠ€νŠΈ 컀버리지

λ‹€μŒμ— λΉ„μŠ·ν•œ ν”„λ‘œμ νŠΈλ₯Ό ν•  κ²½μš°μ—λŠ” ꡬ문, κ²°μ •, 쑰건 브랜치 ν…ŒμŠ€νŠΈ 컀버리지에 더 신경을 μ¨μ„œ 수치λ₯Ό λ†’μ—¬λ³΄λŠ” 것이 λͺ©ν‘œμž…λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ 컀버리지가 μ ˆλŒ€μ μΈ 것은 μ•„λ‹ˆμ§€λ§Œ 높을 수둝 긍정적인 μ‹ ν˜Έμ΄κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

### ❗️ μ—λŸ¬ 핸듀링

보닀 κΌΌκΌΌν•œ μ—λŸ¬ ν•Έλ“€λ§μœΌλ‘œ λ‹€μ–‘ν•œ μ‘°κ±΄μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•΄μ„œ 더 robustν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λ§Œλ“€κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

### ☁️ 배포

`.env` λ“±μ˜ μ‹œν¬λ¦Ώ 파일 뢄리, `p6spy` ν•΄μ œ 등을 μ μš©ν•œ 배포 버전을 톡해 λ°”λ‘œ 배포할 수 μžˆλ„λ‘ λ§Œλ“€κ³  μ‹ΆμŠ΅λ‹ˆλ‹€.

### πŸ“ƒ μš”κ΅¬ 사항 뢄석에 λŒ€ν•΄μ„œ

μš”κ΅¬μ‚¬ν•­μ„ ν•΄μ„ν•˜λŠ” κ³Όμ •μ—μ„œ μ€‘μ˜μ μœΌλ‘œ 해석이 κ°€λŠ₯ν•œ 뢀뢄이 μžˆμ—ˆλŠ”λ°, 고객의 μž…μž₯μ—μ„œ 확인을 ν•œλ²ˆ 더 ν–ˆμ–΄μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€. 끝내고 λ‚˜λ‹ˆ 자의적으둜 ν•΄μ„ν•΄μ„œ 진행을 ν•œ 것 κ°™μ•„ 아쉬움이 λ‚¨μŠ΅λ‹ˆλ‹€.

μ•žμœΌλ‘œ 진행할 경우 μŠ€ν”„λ¦°νŠΈ λ‹¨μœ„λ‘œ μš”κ΅¬μ‚¬ν•­κ³Ό κΈ°λŠ₯이 μ •ν™•νžˆ μΌμΉ˜ν•˜λŠ”μ§€ ν™•μΈν•˜λŠ” 과정을 톡해 μ†Œν†΅μ„ ν•˜λ©° κ°œλ°œν•˜λŠ” 것이 λͺ©ν‘œμž…λ‹ˆλ‹€.