Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
𧳠νΈλ¦¬νμ¬νμ ν΄λ½ λ§μΌλ¦¬μ§ μλΉμ€
- Host: GitHub
- URL: https://github.com/litsynp/triple-club-mileage-service
- Owner: litsynp
- Created: 2022-07-04T14:05:21.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2022-07-28T10:30:01.000Z (over 2 years ago)
- Last Synced: 2023-03-05T02:38:29.545Z (over 1 year ago)
- Topics: docker, mysql, querydsl-jpa, spring-boot, spring-data-jpa, spring-rest-docs
- Language: Java
- Homepage:
- Size: 430 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
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 + 1if 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` ν΄μ λ±μ μ μ©ν λ°°ν¬ λ²μ μ ν΅ν΄ λ°λ‘ λ°°ν¬ν μ μλλ‘ λ§λ€κ³ μΆμ΅λλ€.
### π μꡬ μ¬ν λΆμμ λν΄μ
μꡬμ¬νμ ν΄μνλ κ³Όμ μμ μ€μμ μΌλ‘ ν΄μμ΄ κ°λ₯ν λΆλΆμ΄ μμλλ°, κ³ κ°μ μ μ₯μμ νμΈμ νλ² λ νμ΄μΌ νμ΅λλ€. λλ΄κ³ λλ μμμ μΌλ‘ ν΄μν΄μ μ§νμ ν κ² κ°μ μμ¬μμ΄ λ¨μ΅λλ€.
μμΌλ‘ μ§νν κ²½μ° μ€νλ¦°νΈ λ¨μλ‘ μꡬμ¬νκ³Ό κΈ°λ₯μ΄ μ νν μΌμΉνλμ§ νμΈνλ κ³Όμ μ ν΅ν΄ μν΅μ νλ©° κ°λ°νλ κ²μ΄ λͺ©νμ λλ€.