๐ ํ๋ก์ ํธ ๊ฐ์
1๏ธโฃํ๋ก์ ํธ๋ด์ฉ
์ฌํ, ์ฌ์ ์ ๊ธฐ๋กํ๋ SNS ์๋น์ค 2๋จ๊ณ
2๏ธโฃํ๋ก์ ํธ ์ฃผ์ ๋ฐ ํ์ ๊ตฌํ ๊ธฐ๋ฅ ์ ์
์ผ๋์
3๏ธโฃํ๋ก์ ํธ ๋ชฉ์
Spring Boot, DB ์ค๊ณ, DB ํธ๋์ญ์ , RESTful API ์ค๊ณ ๋ฅ๋ ฅ ํฅ์
4๏ธโฃํ๋ก์ ํธ ๊ธฐ๊ฐ
2023๋ 10์ 23์ผ ~ 2023๋ 10์ 29์ผ
5๏ธโฃTeam Repository
https://github.com/FC-BE-ToyProject-Team8/TravelApp
6๏ธโฃ๊ธฐ์ ์คํ
์ธ์ด
Java17
๊ฐ๋ฐํ๊ฒฝ ๋ฐ Dependency
Spring Boot 3.1.5
Gradle 8.3
MySQL 8
Spring Web
Spring Data JPA
JUint5
lombok
CI
GitHub Actions
API ๋ช ์ธ
Swagger
7๏ธโฃ์๋ฒ ๋ฐฐํฌ
AWS EC2๋ฅผ ์ฌ์ฉํ์ฌ develop , main ๋ธ๋์น ํ ์คํธ ์๋ฒ ๋ฐ ๋ฉ์ธ ์๋ฒ๋ก ์๋ ๋ฐฐํฌ
โ๏ธํ๋ก์ ํธ ์ปจ๋ฒค์
๋ชจ๋ ์ฝ์ ๋ด์ฉ์ ๋ ธ์ ํ์ด์ง์ ์ ๋ฆฌ
๋ชจ๋ ํ์ ๋ด์ฉ์ ํ์๋ก ์์ฑ
1๏ธโฃ์ฝ๋ฉ ์ปจ๋ฒค์
๋ธ๋ก์ ๋ค์ฌ์ฐ๊ธฐ๋ง 4๊ณต๋ฐฑ์ผ๋ก ์ปค์คํ ํ์ฌ ๊ตฌ๊ธ ์๋ฐ ์ฝ๋ฉ ์ปจ๋ฒค์ ์ ์ฌ์ฉ
Git Hub Actions CI๋ฅผ ํตํด ์ฒดํฌ ์คํ์ผ์ด ๋ง์ง ์๋๋ค๋ฉด Merge ๋ชปํ๋๋ก ๋ธ๋ฝํน
2๏ธโฃํ ์คํธ ์ฝ๋ ์ปจ๋ฒค์
Repository : @Query ์ด๋ ธํ ์ด์ ๋ฑ์ ์ฌ์ฉํ ๋ฉ์๋์ ๋ํด ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
Service : ๋จ์ Repository ๋ฉ์๋ ํธ์ถ ์ด์์ ๋ก์ง์ ์ํํ๋ ๋ฉ์๋์ ๋ํด ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
Controller : ๋ชจ๋ public ๋ฉ์๋ ํ ์คํธ ์ฝ๋ ์์ฑ ํ์
3๏ธโฃ์ํ์ฝ๋ ์ปจ๋ฒค์
200 : ๋ชจ๋ ์์ฒญ ์ฑ๊ณต ์ํ์ฝ๋
400 : ๋ฐ์ธ๋ฉ ์๋ฌ ๋ฐ ์๋ชป๋ ์์ฒญ์ ํฌํจํ ๋ชจ๋ ํด๋ผ์ด์ธํธ ์๋ฌ
500 : ๋ชจ๋ ์๋ฒ ๋ด๋ถ ์๋ฌ
4๏ธโฃ์ปค๋ฐ ๋ฉ์ธ์ง ์ปจ๋ฒค์
์ปค๋ฐ ์ ๋ชฉ์ prefix: ์ปค๋ฐ ๋ฉ์์ง ํํ๋ก ์์ฑ
prefix ๋ ๊ฐ๊ฐ ์ํฉ์ ๋ง๋ prefix๋ฅผ ์ฌ์ฉ
(feat, fix, docs, test, refactor...)
๋ชจ๋ ์ปค๋ฐ ๋ฉ์ธ์ง๋ ํ๊ตญ์ด๋ก ์์ฑ
5๏ธโฃํ๋ก์ ํธ ๊น ๋ธ๋์น ์ ๋ต
GitFlow ์ ๋ต ( ํ๋ก์ ํธ ๊ท๋ชจ ํน์ง์ release, hotfix ๋ธ๋์น๋ ์๋ต)
โ๏ธํ๋ก์ ํธ ์ค๊ณ
1๏ธโฃAPI ์ค๊ณ
2๏ธโฃํ๋ก์ ํธ ERD
3๏ธโฃํ๋ก์ ํธ Entity
๐โ๏ธ๋ด๋น ํํธ
1. API : ์ฌ์ ๋ณต์ ๋ฑ๋ก
2. ๋ฐํํ ์๋ฃ ์ค๋น ๋ฐ ๋ฐํ
1๏ธโฃ API : ์ฌ์ ๋ณต์ ๋ฑ๋ก
POST /api/trips/{tripId}/itineraries
Controller
Service
Common utility class - Entity Dto Converter
์์ธ์ฒ๋ฆฌ
ํ ์คํธ ์ฝ๋
ItineraryServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
package kr.co.fastcampus.travel.service;
import static kr.co.fastcampus.travel.TravelTestUtils.createItinerary;
import static kr.co.fastcampus.travel.TravelTestUtils.createItineraryRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createLodgeRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createRoute;
import static kr.co.fastcampus.travel.TravelTestUtils.createRouteRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createStayRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createTrip;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import kr.co.fastcampus.travel.common.exception.EntityNotFoundException;
import kr.co.fastcampus.travel.controller.request.ItineraryRequest;
import kr.co.fastcampus.travel.controller.request.LodgeRequest;
import kr.co.fastcampus.travel.controller.request.RouteRequest;
import kr.co.fastcampus.travel.controller.request.StayRequest;
import kr.co.fastcampus.travel.entity.Itinerary;
import kr.co.fastcampus.travel.entity.Route;
import kr.co.fastcampus.travel.entity.Trip;
import kr.co.fastcampus.travel.repository.ItineraryRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ItineraryServiceTest {
@Mock
private ItineraryRepository itineraryRepository;
@InjectMocks
private ItineraryService itineraryService;
@Test
@DisplayName("์ฌ์ ๋ณต์ ๋ฑ๋ก")
void addItineraries() {
// given
Trip trip = createTrip();
List<ItineraryRequest> requests = IntStream.range(0, 3)
.mapToObj(i -> createItineraryRequest())
.toList();
//when
Trip returnedTrip = itineraryService.addItineraries(trip, requests);
//then
assertThat(returnedTrip).isNotNull();
assertThat(returnedTrip.getItineraries().size()).isEqualTo(3);
}
@Test
@DisplayName("์ฌ์ ๋ณต์ ๋ฑ๋ก ์คํจ")
void addItineraries_fail() {
// given
Trip trip = createTrip();
List<ItineraryRequest> requests = IntStream.range(0, 3)
.mapToObj(i -> createItineraryRequest())
.toList();
Trip otherTrip = createTrip();
//when
Trip returnedTrip = itineraryService.addItineraries(otherTrip, requests);
//then
assertThat(trip).isNotEqualTo(returnedTrip);
assertThat(trip.getItineraries().size()).isNotEqualTo(returnedTrip.getItineraries().size());
}
}
|
cs |
TravelControllerTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
package kr.co.fastcampus.travel.controller;
import static kr.co.fastcampus.travel.TravelTestUtils.createItinerary;
import static kr.co.fastcampus.travel.TravelTestUtils.createItineraryRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createLodgeRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createRouteRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createStayRequest;
import static kr.co.fastcampus.travel.TravelTestUtils.createTrip;
import static kr.co.fastcampus.travel.TravelTestUtils.putAndExtractResponse;
import static kr.co.fastcampus.travel.TravelTestUtils.requestDeleteApi;
import static kr.co.fastcampus.travel.TravelTestUtils.requestFindAllTripApi;
import static kr.co.fastcampus.travel.common.response.Status.FAIL;
import static kr.co.fastcampus.travel.common.response.Status.SUCCESS;
import static kr.co.fastcampus.travel.controller.util.TravelDtoConverter.toTripSummaryResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.junit.jupiter.api.Assertions.assertAll;
import io.restassured.RestAssured;
import io.restassured.path.json.JsonPath;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.IntStream;
import kr.co.fastcampus.travel.ApiTest;
import kr.co.fastcampus.travel.common.response.Status;
import kr.co.fastcampus.travel.controller.request.ItineraryRequest;
import kr.co.fastcampus.travel.controller.request.LodgeRequest;
import kr.co.fastcampus.travel.controller.request.RouteRequest;
import kr.co.fastcampus.travel.controller.request.StayRequest;
import kr.co.fastcampus.travel.controller.request.TripRequest;
import kr.co.fastcampus.travel.controller.response.ItineraryResponse;
import kr.co.fastcampus.travel.controller.response.TripResponse;
import kr.co.fastcampus.travel.controller.response.TripSummaryResponse;
import kr.co.fastcampus.travel.entity.Itinerary;
import kr.co.fastcampus.travel.entity.Trip;
import kr.co.fastcampus.travel.repository.ItineraryRepository;
import kr.co.fastcampus.travel.repository.TripRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
public class TravelControllerTest extends ApiTest {
@Autowired
private TripRepository tripRepository;
@Autowired
private ItineraryRepository itineraryRepository;
@Test
@DisplayName("์ฌ์ ๋ณต์ ๋ฑ๋ก")
void addItineraries() {
// given
Trip trip = createTrip();
tripRepository.save(trip);
String url = "/api/trips/1/itineraries";
List<ItineraryRequest> request = IntStream.range(0, 3)
.mapToObj(i -> createItineraryRequest())
.toList();
//when
ExtractableResponse<Response> response = RestAssured
.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(request)
.when()
.post(url)
.then().log().all()
.extract();
// then
JsonPath jsonPath = response.jsonPath();
String status = jsonPath.getString("status");
TripResponse data = jsonPath.getObject("data", TripResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertSoftly((softly) -> {
softly.assertThat(status).isEqualTo("SUCCESS");
softly.assertThat(data.id()).isEqualTo(1);
softly.assertThat(data.name()).isEqualTo("tripName");
softly.assertThat(data.itineraries().size()).isEqualTo(3);
});
}
private Trip saveTrip() {
Trip trip = createTrip();
return tripRepository.save(trip);
}
private Itinerary saveItinerary(Trip trip) {
Itinerary itinerary = createItinerary(trip);
return itineraryRepository.save(itinerary);
}
}
|
cs |
์์ฐ ๊ฒฐ๊ณผ
2๏ธโฃ๋ฐํํ ์๋ฃ ์ค๋น ๋ฐ ๋ฐํ
๐ฅํํ๊ณ
ํ๋ก์ ํธ๊ฐ ๋๋๊ณ ๋ค๊ฐ์ด ๋ชจ์ฌ์ ํ๋ก์ ํธ์ ๋ํ ๋ฆฌ๋ทฐ ๋ฐ ํผ๋๋ฐฑ, ํ๊ณ ํ๋ ์๊ฐ์ ๊ฐ์ก๋ค.
1. ๊ฒ์ฆ ์คํจ ์ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ง์ (์์๋ณด์ฅ๊น์ง)
2. ์ฌํ ์ข
๋ฃ์ผ์๊ฐ ์ฌํ ์์์ผ์๋ณด๋ค ์์๋์ง์ ๋ํ ์์ธ์ฒ๋ฆฌ
3. Postman ์์ฒญ๋ค์ ๋ํ ๊ณต์ 4. ZonedDateTime ์ฌ์ฉ
5. Create/Update ๋ณ๋ก DTO ๋๋๊ธฐ
6. Controller์ DTO, Service์ DTO
7. Enum ์ด๋์๋จ8. ์์์ผ์/์ข
๋ฃ์ผ์๋ฅผ ๋ค๋ฃจ๋ Entity์ ๊ทธ ์๋์ ๋ชจ์์ ธ์๋ ๊ฒ์ฆ ๋ฉ์๋๋ค (๋ณด๋ฅ)
์์ธ์ฒ๋ฆฌ๋ฅผ ๋ณด๋ค ๋ ์ ๊ฒฝ์จ๋ณด์๋ ์ด์ผ๊ธฐ๊ฐ ๋์๊ณ , RequestDto , ResponseDto ๋ ์ญํ ๋ณ๋ก ์ธ๋ถํํ์ฌ ๋๋์๋ ์ด์ผ๊ธฐ๊ฐ ๋์๋ค. ๋ํ ๊ณ์ธต๋ณ๋ก ์ฐ๋ DTO๋ ๋๋์ด ๊ฒฐํฉ๋๋ฅผ ๋์ฑ ๋ฎ์ถ์๋ ์ด์ผ๊ธฐ๋ฅผ ๋๋ด๋ค. PostMan ํ ์คํธ๋ฅผ ์ ์ฅํ์ฌ ํ์์ด importํ์ฌ ๋ค๊ฐ์ด ์ฌ์ฉํ ์ ์๋๋ก ํ์๊ณ ํ๊ณ , ์ด๋์๋จ์ Enum์ผ๋ก ๋ฃ์๋ ์ด์ผ๊ธฐ๋ ๋์๋ค.
์ํ ์ ์ ๋ค๋ฅธ ์กฐ๋ค์ ๋นํด ๋์ฑ ์ฝ๋๋ฅผ ํด๋ฆฐํ๊ฒ ์งฐ๋ค๋ ์ ๊ณผ ๊ฐ์ ๊ฐ์ธ์ด ๋งก์ API์ ํ ์คํธ์ฝ๋๋ฅผ ์์ฑํ๋ค๋ ์ ์ ์๋ค.
๋๊ธ