diff --git a/.github/workflows/action-develop-cd.yml b/.github/workflows/action-develop-cd.yml index 32f0522..aef7857 100644 --- a/.github/workflows/action-develop-cd.yml +++ b/.github/workflows/action-develop-cd.yml @@ -40,14 +40,14 @@ jobs: chmod +x ./gradlew ./gradlew clean build -x test - # 도커 컴포즈 설정 파일 서버(EC2)로 전달 - - name: Send docker-compose.yml + # 설정 파일 서버(EC2)로 전달 + - name: Send docker-compose.yml and nginx.conf uses: appleboy/scp-action@master with: username: ubuntu host: ${{ secrets.KCS_HOST_DEV }} key: ${{ secrets.KCS_KEY_DEV }} - source: "src/main/resources/backend-submodule/docker-compose-dev.yml" + source: "src/main/resources/backend-submodule/docker-compose-dev.yml,./nginx/nginx.conf" target: "/home/ubuntu/" # Docker hub 로그인 @@ -57,8 +57,8 @@ jobs: username: ${{ secrets.DOCKER_USERNAME}} password: ${{ secrets.DOCKER_TOKEN}} - # Docker Hub 에 푸시 - - name: Build and push + # Docker Hub 에 Springboot 푸시 + - name: Build and push springboot uses: docker/build-push-action@v4 with: context: . diff --git a/.github/workflows/action-production-cd.yml b/.github/workflows/action-production-cd.yml index 39b7d02..6068852 100644 --- a/.github/workflows/action-production-cd.yml +++ b/.github/workflows/action-production-cd.yml @@ -40,14 +40,14 @@ jobs: chmod +x ./gradlew ./gradlew clean build -x test - # 도커 컴포즈 설정 파일 서버로 전달 - - name: Send docker-compose.yml + # 설정 파일 서버로 전달 + - name: Send docker-compose.yml and nginx uses: appleboy/scp-action@master with: username: ${{ secrets.KCS_USERNAME_PROD }} host: ${{ secrets.KCS_HOST_PROD }} key: ${{ secrets.KCS_KEY_PROD }} - source: "src/main/resources/backend-submodule/docker-compose.yml" + source: "src/main/resources/backend-submodule/docker-compose.yml,src/main/resources/backend-submodule/nginx/nginx.conf" target: "/home/g22203/" # Docker hub 로그인 @@ -79,8 +79,9 @@ jobs: script: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} sudo docker pull {{ secrets.DOCKER_REPOSITORY_PROD }}:latest - docker-compose -f docker-compose-dev.yml down + docker-compose -f docker-compose.yml down docker rmi $(docker images -q) cp -f ./src/main/resources/backend-submodule/docker-compose.yml . + cp -rf ./src/main/resources/backend-submodule/nginx . rm -r src docker-compose -f docker-compose.yml up -d \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a0f3503 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,41 @@ +events { + worker_connections 1024; +} + +http { + + # -------------------- spring-boot-dev WAS -------------------- + upstream backend { + server server:8080; + } + + server { + listen 80; + server_name dev.nainga.store; + + # certbot 이 소유자임을 확인하는 경로 + location /.well-known/acme-challenge { + root /var/lib/letsencrypt/; # 사용자 인증을 위한 파일이 생성 되는곳 + } + + # Redirect all traffic to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name dev.nainga.store; + + ssl_certificate /etc/letsencrypt/live/dev.nainga.store/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev.nainga.store/privkey.pem; + + location / { + proxy_pass http://backend; + proxy_set_header Host $host; # 클라이언트가 요청한 호스트의 도메인 + proxy_set_header X-Real-IP $remote_addr; # 클라이언트의 실제 IP 주소 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 원격 클라이언트의 실제 IP 주소와, 이전에 거친 프록시 서버의 IP 주소들이 쉼표로 구분되어 포함 + } + } +} diff --git a/src/main/java/com/nainga/nainga/domain/storecertification/api/StoreCertificationApi.java b/src/main/java/com/nainga/nainga/domain/storecertification/api/StoreCertificationApi.java index f0cad01..4d4dcad 100644 --- a/src/main/java/com/nainga/nainga/domain/storecertification/api/StoreCertificationApi.java +++ b/src/main/java/com/nainga/nainga/domain/storecertification/api/StoreCertificationApi.java @@ -43,9 +43,9 @@ public class StoreCertificationApi { "certificationName: 가게의 인증제 목록
" + "=> 각 인증제별 순서는 보장되지 않습니다.") @GetMapping("api/v1/storecertification/byLocation") - public Result> findStoreCertificationsByLocation(@RequestParam("nwLong") double nwLong, @RequestParam("nwLat") double nwLat, @RequestParam("seLong") double seLong, @RequestParam("seLat") double seLat) { - List storeCertificationsByLocation = storeCertificationService.findStoreCertificationsByLocation(new Location(nwLong, nwLat), new Location(seLong, seLat)); - List storeIdsWithMultipleCertifications = storeCertificationService.findStoreIdsWithMultipleCertifications(); //여러 인증제를 가지고 있는 가게의 id 리스트 + public Result> findStoreCertificationsByLocation(@RequestParam("nwLong") double nwLong, @RequestParam("nwLat") double nwLat, @RequestParam("swLong") double swLong, @RequestParam("swLat") double swLat, @RequestParam("seLong") double seLong, @RequestParam("seLat") double seLat, @RequestParam("neLong") double neLong, @RequestParam("neLat") double neLat) { + List storeCertificationsByLocation = storeCertificationService.findStoreCertificationsByLocation(new Location(nwLong, nwLat), new Location(swLong, swLat), new Location(seLong, seLat), new Location(neLong, neLat)); + List storeIdsWithMultipleCertifications = storeCertificationService.getDuplicatedStoreIds(); //여러 인증제를 가지고 있는 가게의 id 리스트 List storeCertificationsByLocationResponses = new ArrayList<>(); //반환해줄 StoreCertificationsByLocationResponse들의 List Map map = new HashMap<>(); //여러 인증제를 가지고 있는 가게들의 response를 임시로 저장하고 있을 map diff --git a/src/main/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationService.java b/src/main/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationService.java index c0b29e7..1390ef4 100644 --- a/src/main/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationService.java +++ b/src/main/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationService.java @@ -3,35 +3,49 @@ import com.nainga.nainga.domain.store.domain.Location; import com.nainga.nainga.domain.storecertification.dao.StoreCertificationRepository; import com.nainga.nainga.domain.storecertification.domain.StoreCertification; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; @Service @Transactional(readOnly = true) -@RequiredArgsConstructor public class StoreCertificationService { private final StoreCertificationRepository storeCertificationRepository; - public List findStoreCertificationsByLocation(Location northWestLocation, Location southEastLocation) { - return storeCertificationRepository.findStoreCertificationsByLocation(northWestLocation, southEastLocation); + private List duplicatedStoreIds; //여러 인증제를 가지는 중복된 storeId를 담고있는 리스트 + + @Autowired + public StoreCertificationService(StoreCertificationRepository storeCertificationRepository) { + this.storeCertificationRepository = storeCertificationRepository; } - public List findStoreIdsWithMultipleCertifications() { + @PostConstruct + public void init() { //이 Service Bean이 생성된 이후에 한번만 중복된 storeId를 검사해서 Globally하게 저장 List allStoreCertifications = storeCertificationRepository.findAll(); //중복된 id를 검사하기 위함 - List allStoreIds = new ArrayList<>(); + + HashSet uniqueStoreIds = new HashSet<>(); //조회 성능을 높이기 위해 HashSet으로 저장 + HashSet duplicatedIds = new HashSet<>(); + for (StoreCertification storeCertification : allStoreCertifications) { - allStoreIds.add(storeCertification.getStore().getId()); + Long storeId = storeCertification.getStore().getId(); + if (!uniqueStoreIds.add(storeId)) { //HashSet에 add를 했을 때 이미 존재하는 데이터면 false가 리턴되는 것을 활용 + duplicatedIds.add(storeId); + } } + duplicatedStoreIds = new ArrayList<>(duplicatedIds); + } - List duplicatedIds = allStoreIds.stream() - .filter(e -> allStoreIds.indexOf(e) != allStoreIds.lastIndexOf(e)) //중복된 StoreId가 있는 경우 - .distinct() //해당 id를 모아서 1번씩만(중복 제거) 리스트에 담아 전달 - .collect(Collectors.toList()); + public List findStoreCertificationsByLocation(Location northWestLocation, Location southWestLocation, Location southEastLocation, Location northEastLocation) { + return storeCertificationRepository.findStoreCertificationsByLocation(northWestLocation, southWestLocation, southEastLocation, northEastLocation); + } - return duplicatedIds; + public List getDuplicatedStoreIds() { + return duplicatedStoreIds; } } diff --git a/src/main/java/com/nainga/nainga/domain/storecertification/dao/StoreCertificationRepository.java b/src/main/java/com/nainga/nainga/domain/storecertification/dao/StoreCertificationRepository.java index c7cea75..5f8df13 100644 --- a/src/main/java/com/nainga/nainga/domain/storecertification/dao/StoreCertificationRepository.java +++ b/src/main/java/com/nainga/nainga/domain/storecertification/dao/StoreCertificationRepository.java @@ -39,14 +39,14 @@ public Optional findByStoreIdCertificationId(Long storeId, L return result.stream().findAny(); } - //북서쪽 좌표와 남동쪽 좌표를 받아 그 두 좌표로 만들어지는 최소 사각형 내에 위치하는 가게들 리턴 - public List findStoreCertificationsByLocation(Location northWestLocation, Location southEastLocation) { + //북서쪽 좌표, 남서쪽 좌표, 남동쪽 좌표, 북동쪽 좌표를 받아 그 네 좌표로 만들어지는 사각형 영역 내에 위치하는 가게들 리턴 + public List findStoreCertificationsByLocation(Location northWestLocation, Location southWestLocation, Location southEastLocation, Location northEastLocation) { String pointFormat = String.format( - "'LINESTRING(%f %f, %f %f)'", //POINT는 (경도, 위도) 순이다. 즉, (Logitude, Latitude)순 - northWestLocation.getLongitude(), northWestLocation.getLatitude(), southEastLocation.getLongitude(), southEastLocation.getLatitude() + "'POLYGON((%f %f, %f %f, %f %f, %f %f, %f %f))'", //POINT는 (경도, 위도) 순이다. 즉, (Logitude, Latitude)순 + northWestLocation.getLongitude(), northWestLocation.getLatitude(), southWestLocation.getLongitude(), southWestLocation.getLatitude(), southEastLocation.getLongitude(), southEastLocation.getLatitude(), northEastLocation.getLongitude(), northEastLocation.getLatitude(), northWestLocation.getLongitude(), northWestLocation.getLatitude() ); - Query query = em.createNativeQuery("SELECT sc.* " + "FROM store_certification AS sc " + "JOIN store AS s ON sc.store_id = s.store_id " + "JOIN certification AS c ON sc.certification_id = c.certification_id " + "WHERE MBRCONTAINS(ST_LINESTRINGFROMTEXT(" + pointFormat + "), s.location)", StoreCertification.class); + Query query = em.createNativeQuery("SELECT sc.* " + "FROM store_certification AS sc " + "JOIN store AS s ON sc.store_id = s.store_id " + "JOIN certification AS c ON sc.certification_id = c.certification_id " + "WHERE ST_CONTAINS(ST_POLYGONFROMTEXT(" + pointFormat + "), s.location)", StoreCertification.class); return query.getResultList(); } diff --git a/src/main/java/com/nainga/nainga/global/config/SwaggerConfig.java b/src/main/java/com/nainga/nainga/global/config/SwaggerConfig.java new file mode 100644 index 0000000..0472428 --- /dev/null +++ b/src/main/java/com/nainga/nainga/global/config/SwaggerConfig.java @@ -0,0 +1,43 @@ +package com.nainga.nainga.global.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.tags.Tag; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + //Google Cloud Storage API는 별도로 Swagger에 명세 + return new OpenAPI() + .paths(new Paths().addPathItem("https://storage.googleapis.com/{BUCKET_NAME}/{IMAGE_NAME}", + new PathItem().get(new Operation().summary("저장된 가게 이미지 제공") + .description("저장된 가게 이미지를 제공하는 API입니다.
" + + "본 API는 Google Cloud Storage에서 제공하는 API로 URL이 위와 같으며 이 정보는 각 가게별 local_photos 필드에 저장되어 있습니다.
" + + "Dev 환경에서 BUCKET_NAME은 kcs-dev-bucket1이고 Prod 환경에서 BUCKET_NAME은 kcs-prod-bucket1입니다.
" + + "가게 이름은 UUID를 활용한 난수로 제공됩니다.
" + + "참고로 Swagger 상에서는 Base URL이 달라 테스트가 불가능합니다.
" + + "만약 테스트를 원하신다면 브라우저 상에서 직접 URL을 입력해주시면 됩니다.
" + + "예) https://storage.googleapis.com/kcs-dev-bucket1/ad06294c-d4ed-42bd-9839-82af8714bd1e") + .tags(List.of("가게 상세 정보")) + .responses(new ApiResponses().addApiResponse("200", + new ApiResponse().description("OK") + .content(new Content().addMediaType("image/jpeg", new MediaType() + .schema(new Schema<>().type("string") + .format("binary"))))))))); + } +} + diff --git a/src/main/resources/backend-submodule b/src/main/resources/backend-submodule index 614a5b7..67e3a62 160000 --- a/src/main/resources/backend-submodule +++ b/src/main/resources/backend-submodule @@ -1 +1 @@ -Subproject commit 614a5b75a7bd04035791b642f83a7c72e21dba6f +Subproject commit 67e3a628d394ee510efccd846b3e70e63ce63e42 diff --git a/src/test/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationServiceTest.java b/src/test/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationServiceTest.java index 68b9b67..142b24e 100644 --- a/src/test/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationServiceTest.java +++ b/src/test/java/com/nainga/nainga/domain/storecertification/application/StoreCertificationServiceTest.java @@ -55,9 +55,11 @@ void findStoreCertificationsByLocation() { } //when - Location location1 = new Location(minLongitude - 1.0, minLatitude - 1.0); - Location location2 = new Location(maxLongitude + 1.0, maxLatitude + 1.0); - List storeCertificationsByLocation = storeCertificationService.findStoreCertificationsByLocation(location1, location2); + Location location1 = new Location(minLongitude - 1.0, maxLatitude + 1.0); + Location location2 = new Location(minLongitude - 1.0, minLatitude - 1.0); + Location location3 = new Location(maxLongitude + 1.0, minLatitude - 1.0); + Location location4 = new Location(maxLongitude + 1.0, maxLatitude + 1.0); + List storeCertificationsByLocation = storeCertificationService.findStoreCertificationsByLocation(location1, location2, location3, location4); //then assertThat(storeCertificationsByLocation.size()).isEqualTo(stores.size()); diff --git a/src/test/resources/backend-submodule b/src/test/resources/backend-submodule index 614a5b7..67e3a62 160000 --- a/src/test/resources/backend-submodule +++ b/src/test/resources/backend-submodule @@ -1 +1 @@ -Subproject commit 614a5b75a7bd04035791b642f83a7c72e21dba6f +Subproject commit 67e3a628d394ee510efccd846b3e70e63ce63e42