From 6e10d20322d0f6ac6bef858fb5cefa7cfcb846d1 Mon Sep 17 00:00:00 2001 From: KMINGON Date: Tue, 16 Apr 2024 02:52:02 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20=ED=96=89=EC=82=AC=20=ED=91=B8?= =?UTF-8?q?=EB=93=9C=ED=8A=B8=EB=9F=AD=20=EC=A7=80=EC=9B=90=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/MapStructMapperImpl.java | 4 +- .../luck/configuration/MapStructMapper.java | 1 + .../ac/kr/deu/connect/luck/event/Event.java | 4 +- .../connect/luck/event/EventController.java | 8 +++ .../deu/connect/luck/event/EventService.java | 2 +- .../deu/connect/luck/event/EventStatus.java | 9 +++ .../eventApplication/ApplicationStatus.java | 8 +++ .../EventApplicationMapper.java | 12 ++++ .../EventApplicationRequest.java | 8 +++ .../EventApplicationRestController.java | 31 +++++++++ .../EventApplicationService.java | 63 +++++++++++++++++++ .../luck/exception/CustomErrorCode.java | 3 +- .../resources/templates/event/event-form.html | 7 +++ 13 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ac/kr/deu/connect/luck/event/EventStatus.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java diff --git a/src/main/generated/ac/kr/deu/connect/luck/configuration/MapStructMapperImpl.java b/src/main/generated/ac/kr/deu/connect/luck/configuration/MapStructMapperImpl.java index 87ba2a0..6b22eb7 100644 --- a/src/main/generated/ac/kr/deu/connect/luck/configuration/MapStructMapperImpl.java +++ b/src/main/generated/ac/kr/deu/connect/luck/configuration/MapStructMapperImpl.java @@ -23,8 +23,8 @@ @Generated( value = "org.mapstruct.ap.MappingProcessor", - date = "2024-04-01T12:57:49+0900", - comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.2 (Oracle Corporation)" + date = "2024-04-16T02:14:38+0900", + comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.10 (Oracle Corporation)" ) @Component public class MapStructMapperImpl implements MapStructMapper { diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java b/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java index 13dade6..14775ce 100644 --- a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java +++ b/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java @@ -19,6 +19,7 @@ public interface MapStructMapper { User toUser(LoginRequest loginRequest); + @Mapping(target = "status", defaultValue = "BEFORE_APPLICATION", ignore = true ) @Mapping(source = "managerId", target = "manager.id") Event toEvent(EventRequest eventRequest); diff --git a/src/main/java/ac/kr/deu/connect/luck/event/Event.java b/src/main/java/ac/kr/deu/connect/luck/event/Event.java index 70943ab..34b359a 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event/Event.java +++ b/src/main/java/ac/kr/deu/connect/luck/event/Event.java @@ -28,4 +28,6 @@ public class Event extends BaseEntity { private String imageUrl; @ManyToOne private User manager; -} + @Enumerated(EnumType.STRING) + private EventStatus status; +} \ No newline at end of file diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventController.java b/src/main/java/ac/kr/deu/connect/luck/event/EventController.java index ef96164..ccaac7d 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event/EventController.java +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventController.java @@ -5,6 +5,7 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PathVariable; @Controller @RequestMapping("/event") @@ -18,4 +19,11 @@ public String getEvent(Model model) { model.addAttribute("events", eventService.getEvents()); return "event/event-list"; } + + @GetMapping("/{id}") + public String getEventDetail(@PathVariable("id") Long id, Model model) { + model.addAttribute("event", eventService.getEvent(id)); + return "event/event-form"; + } + } diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventService.java b/src/main/java/ac/kr/deu/connect/luck/event/EventService.java index c116405..94d9fab 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event/EventService.java +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventService.java @@ -39,4 +39,4 @@ public Event updateEvent(Long id, EventRequest eventRequest) { public void deleteEvent(Long id) { eventRepository.deleteById(id); } -} +} \ No newline at end of file diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventStatus.java b/src/main/java/ac/kr/deu/connect/luck/event/EventStatus.java new file mode 100644 index 0000000..93a2973 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventStatus.java @@ -0,0 +1,9 @@ +package ac.kr.deu.connect.luck.event; + +public enum EventStatus { + BEFORE_APPLICATION, // 모집 전 + OPEN_FOR_APPLICATION, // 신청 가능 + APPLICATION_EXPIRED, // 신청 만료 + EVENT_START, // 행사 시작 + EVENT_END // 행사 끝 +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java b/src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java new file mode 100644 index 0000000..fa5a37c --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java @@ -0,0 +1,8 @@ +package ac.kr.deu.connect.luck.eventApplication; + +public enum ApplicationStatus { + PENDING, // 요청 중 + APPROVED, // 승인됨 + REJECTED, // 거절됨 + CANCELED // 취소됨 +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java new file mode 100644 index 0000000..8af290b --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java @@ -0,0 +1,12 @@ +package ac.kr.deu.connect.luck.eventApplication; + +import ac.kr.deu.connect.luck.Application; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface EventApplicationMapper { + + @Mapping(source = "comment", target = "comment") + EventApplication toEventApplication(EventApplicationRequest eventApplicationRequest); +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java new file mode 100644 index 0000000..c23c243 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java @@ -0,0 +1,8 @@ +package ac.kr.deu.connect.luck.eventApplication; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record EventApplicationRequest( + @Schema(description = "신청 메시지", example = "죠희 타코야키 잘 만들어요") + String comment) { +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java new file mode 100644 index 0000000..c19ada4 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java @@ -0,0 +1,31 @@ +package ac.kr.deu.connect.luck.eventApplication; + + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@AllArgsConstructor +public class EventApplicationRestController { + + private final EventApplicationService eventApplicationService; + + @PostMapping("event/{id}/apply") + public void applyEvent( + @PathVariable(name = "id") Long id, + @RequestBody EventApplicationRequest eventApplicationRequest, + @RequestHeader(name = "username") String username + ) { + log.info("requestbody : {}",eventApplicationRequest.comment()); + eventApplicationService.createEventApplication(eventApplicationRequest, id, username); + } + + @GetMapping("event/apply") + public List getEventApplications(){ + return eventApplicationService.getEventApplications(); + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java new file mode 100644 index 0000000..14d48ad --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java @@ -0,0 +1,63 @@ +package ac.kr.deu.connect.luck.eventApplication; + +import ac.kr.deu.connect.luck.event.Event; +import ac.kr.deu.connect.luck.event.EventRepository; +import ac.kr.deu.connect.luck.exception.CustomErrorCode; +import ac.kr.deu.connect.luck.exception.CustomException; +import ac.kr.deu.connect.luck.user.User; +import ac.kr.deu.connect.luck.user.UserRepository; +import ac.kr.deu.connect.luck.user.UserRole; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@AllArgsConstructor +public class EventApplicationService { + + private final EventApplicationRepository eventApplicationRepository; + private final EventApplicationMapper eventApplicationMapper; + private final UserRepository userRepository; + private final EventRepository eventRepository; + + + /** + * 지원서 신규 작성 메소드 + * + * @param eventApplicationRequest 이벤트 요청 폼 + * @param eventId 이벤트 키 + * @param email 신청자(푸드트럭 매니저) 이메일 + * @return 생성된 신규 지원서 + */ + public EventApplication createEventApplication(EventApplicationRequest eventApplicationRequest, Long eventId, String email) { + EventApplication eventApplicationSaved = eventApplicationMapper.toEventApplication(eventApplicationRequest); + + // 유저 정보 조회 + User user = userRepository.findByEmail(email).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); + + if (user.getRole() != UserRole.FOOD_TRUCK_MANAGER){ + throw new CustomException(CustomErrorCode.USER_ID_NOT_MATCH); + } + + Event evnet = eventRepository.findById(eventId).orElseThrow(() -> new CustomException(CustomErrorCode.EVENT_NOT_FOUND)); + + eventApplicationSaved.setFoodTruckManager(user); + eventApplicationSaved.setEvent(evnet); + eventApplicationSaved.setStatus(ApplicationStatus.PENDING); + + return eventApplicationRepository.save(eventApplicationSaved); + } + + //TODO: 지원서 취소(푸드트럭 매니저용) + + //TODO: 지원서 status 상태 변경(승인,거절) (행사 관리자용) + + //TODO: 지원서 조회(푸드트럭 매니저용) + //TODO: 지원서 조회(행사 관리자용) + + //TODO: 모든 지원서 조회 + List getEventApplications() { + return eventApplicationRepository.findAll(); + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java b/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java index 96ff0fd..4ab7170 100644 --- a/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java +++ b/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java @@ -19,7 +19,8 @@ public enum CustomErrorCode { ROLE_NOT_MATCH(HttpStatus.BAD_REQUEST, "권한이 일치하지 않습니다."), FOOD_TRUCK_MENU_NOT_FOUND(HttpStatus.BAD_REQUEST, "푸드트럭 메뉴를 찾을 수 없습니다."), FOOD_TRUCK_IS_ALREADY_OPERATING(HttpStatus.BAD_REQUEST, "푸드트럭이 이미 운영중입니다."), - FOOD_TRUCK_IS_NOT_OPERATING(HttpStatus.BAD_REQUEST, "푸드트럭이 운영중이 아닙니다."); + FOOD_TRUCK_IS_NOT_OPERATING(HttpStatus.BAD_REQUEST, "푸드트럭이 운영중이 아닙니다."), + EVENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "이벤트가 존재하지 않습니다."); private final HttpStatus status; diff --git a/src/main/resources/templates/event/event-form.html b/src/main/resources/templates/event/event-form.html index e69de29..8a89b13 100644 --- a/src/main/resources/templates/event/event-form.html +++ b/src/main/resources/templates/event/event-form.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From 3910e4dbaf66640fa3daa581232549aab053b76e Mon Sep 17 00:00:00 2001 From: Namju Kim Date: Tue, 2 Apr 2024 02:03:48 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Build&Test.yml | 3 + .../luck/exception/CustomErrorCode.java | 3 +- .../luck/image/ImageRestController.java | 28 +++++++++ .../luck/image/ImageUploadResponse.java | 45 ++++++++++++++ .../deu/connect/luck/image/ImageUploader.java | 58 ++++++++++++++++++ src/main/resources/application.yaml | 19 ++++++ .../connect/luck/image/ImageUploaderTest.java | 37 +++++++++++ src/test/resources/test.jpg | Bin 0 -> 44067 bytes 8 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ac/kr/deu/connect/luck/image/ImageRestController.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/image/ImageUploadResponse.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/image/ImageUploader.java create mode 100644 src/test/java/ac/kr/deu/connect/luck/image/ImageUploaderTest.java create mode 100644 src/test/resources/test.jpg diff --git a/.github/workflows/Build&Test.yml b/.github/workflows/Build&Test.yml index 2955a63..40ca5de 100644 --- a/.github/workflows/Build&Test.yml +++ b/.github/workflows/Build&Test.yml @@ -27,6 +27,9 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + - name: Set Environment Variables + run: echo "IMGBB_API_KEY=${{ secrets.IMGBB_API_KEY }}" >> $GITHUB_ENV + - name: Test with Gradle run: ./gradlew --info test diff --git a/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java b/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java index 4ab7170..61af719 100644 --- a/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java +++ b/src/main/java/ac/kr/deu/connect/luck/exception/CustomErrorCode.java @@ -20,7 +20,8 @@ public enum CustomErrorCode { FOOD_TRUCK_MENU_NOT_FOUND(HttpStatus.BAD_REQUEST, "푸드트럭 메뉴를 찾을 수 없습니다."), FOOD_TRUCK_IS_ALREADY_OPERATING(HttpStatus.BAD_REQUEST, "푸드트럭이 이미 운영중입니다."), FOOD_TRUCK_IS_NOT_OPERATING(HttpStatus.BAD_REQUEST, "푸드트럭이 운영중이 아닙니다."), - EVENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "이벤트가 존재하지 않습니다."); + EVENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "이벤트가 존재하지 않습니다."), + IMAGE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "이미지 업로드에 실패했습니다."); private final HttpStatus status; diff --git a/src/main/java/ac/kr/deu/connect/luck/image/ImageRestController.java b/src/main/java/ac/kr/deu/connect/luck/image/ImageRestController.java new file mode 100644 index 0000000..3fa5d3b --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/image/ImageRestController.java @@ -0,0 +1,28 @@ +package ac.kr.deu.connect.luck.image; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@Tag(name = "Image", description = "Image API") +@RequestMapping("/api/image") +@AllArgsConstructor +public class ImageRestController { + + private final ImageUploader imageUploader; + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public String uploadImage( + @Parameter(description = "multipart/form-data 형식의 이미지 리스트를 input으로 받습니다. 이때 key 값은 image 입니다.") + @RequestPart("image") MultipartFile multipartFile) { + return imageUploader.uploadImage(multipartFile).getData().getUrl(); + } + +} diff --git a/src/main/java/ac/kr/deu/connect/luck/image/ImageUploadResponse.java b/src/main/java/ac/kr/deu/connect/luck/image/ImageUploadResponse.java new file mode 100644 index 0000000..1ba7fb9 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/image/ImageUploadResponse.java @@ -0,0 +1,45 @@ +package ac.kr.deu.connect.luck.image; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class ImageUploadResponse { + private Data data; + private boolean success; + private int status; + + @Getter + @Setter + @ToString + public static class Data { + private String id; + private String title; + private String urlViewer; + private String url; + private String displayUrl; + private String width; + private String height; + private String size; + private String time; + private String expiration; + private ImageInfo image; + private ImageInfo thumb; + private ImageInfo medium; + private String deleteUrl; + } + + @Getter + @Setter + @ToString + public static class ImageInfo { + private String filename; + private String name; + private String mime; + private String extension; + private String url; + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/image/ImageUploader.java b/src/main/java/ac/kr/deu/connect/luck/image/ImageUploader.java new file mode 100644 index 0000000..0f5759f --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/image/ImageUploader.java @@ -0,0 +1,58 @@ +package ac.kr.deu.connect.luck.image; + +import ac.kr.deu.connect.luck.exception.CustomErrorCode; +import ac.kr.deu.connect.luck.exception.CustomException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Base64; + +@Component +@Slf4j +public class ImageUploader { + + @Value("${imgbb.api-key}") + private String API_KEY; + + private String BASE_URL = "https://api.imgbb.com/1/upload"; + + private Long EXPIRATION = 15552000L; + + private RestTemplate restTemplate = new RestTemplate(); + + + public ImageUploadResponse uploadImage(MultipartFile file) { + try { + byte[] image = file.getBytes(); + String base64Image = Base64.getEncoder().encodeToString(image); + + String apiUrl = String.format("%s?key=%s&expiration=%d", BASE_URL, API_KEY, EXPIRATION); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap bodyMap = new LinkedMultiValueMap<>(); + bodyMap.add("image", base64Image); + + HttpEntity> request = new HttpEntity<>(bodyMap, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, request, ImageUploadResponse.class); + + log.info("Image upload response: {}", response.getBody()); + log.info("Image upload URL: {}", response.getBody().getData().getUrl()); + return response.getBody(); + } catch (Exception e) { + log.error("Image upload failed: {}", e.getMessage()); + throw new CustomException(CustomErrorCode.IMAGE_UPLOAD_FAILED); + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index fd51b03..5dfd559 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -13,6 +13,11 @@ spring: mode: embedded encoding: utf-8 + servlet: + multipart: + max-file-size: 32MB + max-request-size: 32MB + logging: level: @@ -27,3 +32,17 @@ server: encoding: charset: UTF-8 force-response: true + +### Uncomment the following block to enable HTTPS and HTTP/2 +# port: 8443 +# +# http2: +# enabled: true +# +# ssl: +# enabled: true +# certificate-private-key: "classpath:privkey.pem" +# certificate: "classpath:fullchain.pem" + +imgbb: + api-key: ${IMGBB_API_KEY} diff --git a/src/test/java/ac/kr/deu/connect/luck/image/ImageUploaderTest.java b/src/test/java/ac/kr/deu/connect/luck/image/ImageUploaderTest.java new file mode 100644 index 0000000..f9e0c7f --- /dev/null +++ b/src/test/java/ac/kr/deu/connect/luck/image/ImageUploaderTest.java @@ -0,0 +1,37 @@ +package ac.kr.deu.connect.luck.image; + +import ac.kr.deu.connect.luck.image.ImageUploader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; + +import java.io.FileInputStream; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +class ImageUploaderTest { + + @Autowired + private ImageUploader imageUploader; + + @Test + @DisplayName("이미지 업로드 테스트") + void uploadImage() throws Exception { + // Given + final String filePath = "src/test/resources/test.jpg"; + FileInputStream fis = new FileInputStream(filePath); + MockMultipartFile file = new MockMultipartFile("file", fis); + + // When + ImageUploadResponse imageUrl = imageUploader.uploadImage(file); + + // Then + assertEquals(200, imageUrl.getStatus()); + assertNotNull(imageUrl.getData().getUrl()); + } + +} diff --git a/src/test/resources/test.jpg b/src/test/resources/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfee281ac933b0a8d9c51cc0f4c434ce604038f6 GIT binary patch literal 44067 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 S|DpoyLy=?6?R=ZzU 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 CciX=J literal 0 HcmV?d00001 From 704218fa7436727812ab8932d4378625a277f5f3 Mon Sep 17 00:00:00 2001 From: Namju Kim Date: Tue, 16 Apr 2024 02:57:57 +0900 Subject: [PATCH 03/11] =?UTF-8?q?refactor:=20food-truck=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../luck/configuration/MapStructMapper.java | 9 +++++++-- .../{ => controller}/FoodTruckController.java | 3 ++- .../controller/FoodTruckMenuController.java | 9 +++++++++ .../FoodTruckMenuRestController.java | 5 ++++- .../{ => controller}/FoodTruckRestController.java | 7 ++++++- .../controller/FoodTruckReviewController.java | 12 ++++++++++++ .../controller/FoodTruckReviewRestController.java | 12 ++++++++++++ .../{ => dto}/FoodTruckDetailResponse.java | 3 ++- .../{ => dto}/FoodTruckMenuRequest.java | 2 +- .../{ => dto}/FoodTruckMenuResponse.java | 2 +- .../food_truck/{ => dto}/FoodTruckRequest.java | 3 ++- .../{ => dto}/FoodTruckReviewRequest.java | 2 +- .../{ => dto}/FoodTruckReviewResponse.java | 2 +- .../luck/food_truck/{ => entity}/FoodTruck.java | 2 +- .../food_truck/{ => entity}/FoodTruckMenu.java | 2 +- .../food_truck/{ => entity}/FoodTruckReview.java | 2 +- .../luck/food_truck/{ => entity}/FoodType.java | 2 +- .../{ => repository}/FoodTruckMenuRepository.java | 4 +++- .../{ => repository}/FoodTruckRepository.java | 4 +++- .../FoodTruckReviewRepository.java | 4 +++- .../{ => service}/FoodTruckMenuService.java | 15 ++++++++++----- .../{ => service}/FoodTruckReviewService.java | 4 +++- .../{ => service}/FoodTruckService.java | 11 ++++++++++- src/main/java/ac/kr/deu/connect/luck/now/Now.java | 2 +- .../ac/kr/deu/connect/luck/now/NowService.java | 4 ++-- .../ac/kr/deu/connect/luck/user/UserInfo.java | 2 +- .../ac/kr/deu/connect/luck/user/UserService.java | 6 +++++- .../FoodTruckMenuRestControllerTest.java | 2 ++ .../food_truck/FoodTruckRestControllerTest.java | 4 ++++ 29 files changed, 111 insertions(+), 30 deletions(-) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => controller}/FoodTruckController.java (89%) create mode 100644 src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuController.java rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => controller}/FoodTruckMenuRestController.java (88%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => controller}/FoodTruckRestController.java (86%) create mode 100644 src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewController.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewRestController.java rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckDetailResponse.java (90%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckMenuRequest.java (91%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckMenuResponse.java (95%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckRequest.java (83%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckReviewRequest.java (92%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => dto}/FoodTruckReviewResponse.java (95%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => entity}/FoodTruck.java (91%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => entity}/FoodTruckMenu.java (90%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => entity}/FoodTruckReview.java (91%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => entity}/FoodType.java (73%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => repository}/FoodTruckMenuRepository.java (70%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => repository}/FoodTruckRepository.java (76%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => repository}/FoodTruckReviewRepository.java (75%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => service}/FoodTruckMenuService.java (75%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => service}/FoodTruckReviewService.java (67%) rename src/main/java/ac/kr/deu/connect/luck/food_truck/{ => service}/FoodTruckService.java (89%) diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java b/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java index 14775ce..2f8a078 100644 --- a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java +++ b/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java @@ -4,8 +4,13 @@ import ac.kr.deu.connect.luck.auth.SignUpRequest; import ac.kr.deu.connect.luck.event.Event; import ac.kr.deu.connect.luck.event.EventRequest; -import ac.kr.deu.connect.luck.food_truck.*; -import ac.kr.deu.connect.luck.now.Now; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckDetailResponse; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckRequest; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckReviewRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; import ac.kr.deu.connect.luck.user.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckController.java similarity index 89% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckController.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckController.java index b168ece..1d83a35 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckController.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckController.java @@ -1,5 +1,6 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.controller; +import ac.kr.deu.connect.luck.food_truck.service.FoodTruckService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuController.java new file mode 100644 index 0000000..cff5d38 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuController.java @@ -0,0 +1,9 @@ +package ac.kr.deu.connect.luck.food_truck.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/food-truck/menu") +public class FoodTruckMenuController { +} diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuRestController.java similarity index 88% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestController.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuRestController.java index 33ab448..0eb62e6 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckMenuRestController.java @@ -1,5 +1,8 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.controller; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; +import ac.kr.deu.connect.luck.food_truck.service.FoodTruckMenuService; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckRestController.java similarity index 86% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestController.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckRestController.java index 6d91a30..f4317b9 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckRestController.java @@ -1,5 +1,10 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.controller; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckDetailResponse; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; +import ac.kr.deu.connect.luck.food_truck.service.FoodTruckService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewController.java new file mode 100644 index 0000000..464d77a --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewController.java @@ -0,0 +1,12 @@ +package ac.kr.deu.connect.luck.food_truck.controller; + +import ac.kr.deu.connect.luck.food_truck.service.FoodTruckReviewService; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class FoodTruckReviewController { + + private final FoodTruckReviewService foodTruckReviewService; +} diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewRestController.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewRestController.java new file mode 100644 index 0000000..0cba654 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/controller/FoodTruckReviewRestController.java @@ -0,0 +1,12 @@ +package ac.kr.deu.connect.luck.food_truck.controller; + +import ac.kr.deu.connect.luck.food_truck.service.FoodTruckReviewService; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class FoodTruckReviewRestController { + + private final FoodTruckReviewService foodTruckReviewService; +} diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckDetailResponse.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckDetailResponse.java similarity index 90% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckDetailResponse.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckDetailResponse.java index bf2bed8..98ba95e 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckDetailResponse.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckDetailResponse.java @@ -1,5 +1,6 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; import ac.kr.deu.connect.luck.user.User; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRequest.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuRequest.java similarity index 91% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRequest.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuRequest.java index b52a63a..a45bb08 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuRequest.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuResponse.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuResponse.java similarity index 95% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuResponse.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuResponse.java index 309501f..a219d71 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuResponse.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckMenuResponse.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRequest.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckRequest.java similarity index 83% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRequest.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckRequest.java index 4ace433..5fa4356 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckRequest.java @@ -1,5 +1,6 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; import io.swagger.v3.oas.annotations.media.Schema; public record FoodTruckRequest( diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRequest.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewRequest.java similarity index 92% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRequest.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewRequest.java index 7174e86..7c3a1e7 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewRequest.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewResponse.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewResponse.java similarity index 95% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewResponse.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewResponse.java index fac17f7..9a0c5dd 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewResponse.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/dto/FoodTruckReviewResponse.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruck.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruck.java similarity index 91% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruck.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruck.java index da270bc..796e2dd 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruck.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruck.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.entity; import ac.kr.deu.connect.luck.common.BaseEntity; import ac.kr.deu.connect.luck.user.User; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenu.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckMenu.java similarity index 90% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenu.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckMenu.java index 60e9486..d8c4967 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenu.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckMenu.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.entity; import ac.kr.deu.connect.luck.common.BaseEntity; import jakarta.persistence.*; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReview.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckReview.java similarity index 91% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReview.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckReview.java index 3423a27..bb73f7c 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReview.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodTruckReview.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.entity; import ac.kr.deu.connect.luck.common.BaseEntity; import ac.kr.deu.connect.luck.user.User; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodType.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodType.java similarity index 73% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodType.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodType.java index e282308..3b9233c 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodType.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/entity/FoodType.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.entity; public enum FoodType { BURGER, CHICKEN, DESSERT, DRINK, HOTDOG, NOODLE, PIZZA, RICE, SALAD, SANDWICH, SNACK, SOUP, STEAK, SUSHI, ETC diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRepository.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckMenuRepository.java similarity index 70% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRepository.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckMenuRepository.java index 6e8669f..56e02d1 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRepository.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckMenuRepository.java @@ -1,5 +1,7 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.repository; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRepository.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java similarity index 76% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRepository.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java index 187e405..b8639e3 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRepository.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java @@ -1,5 +1,7 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.repository; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; import ac.kr.deu.connect.luck.user.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRepository.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckReviewRepository.java similarity index 75% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRepository.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckReviewRepository.java index 16634e7..5893842 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewRepository.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckReviewRepository.java @@ -1,5 +1,7 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.repository; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; import ac.kr.deu.connect.luck.user.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java similarity index 75% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuService.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java index 1e974de..795e984 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java @@ -1,8 +1,13 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.service; import ac.kr.deu.connect.luck.configuration.MapStructMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckMenuRepository; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; import ac.kr.deu.connect.luck.user.User; import ac.kr.deu.connect.luck.user.UserRepository; import ac.kr.deu.connect.luck.user.UserRole; @@ -20,18 +25,18 @@ public class FoodTruckMenuService { private final UserRepository userRepository; private final MapStructMapper mapStructMapper; - List getFoodTruckMenus(Long foodTruckId) { + public List getFoodTruckMenus(Long foodTruckId) { return foodTruckMenuRepository.findByFoodTruckId(foodTruckId); } - FoodTruckMenu saveFoodTruckMenu(Long userId, Long foodTruckId, FoodTruckMenuRequest foodTruckMenuRequest) { + public FoodTruckMenu saveFoodTruckMenu(Long userId, Long foodTruckId, FoodTruckMenuRequest foodTruckMenuRequest) { isManager(userId, foodTruckId); FoodTruckMenu foodTruckMenu = mapStructMapper.toFoodTruckMenu(foodTruckMenuRequest); foodTruckMenu.setFoodTruck(foodTruckRepository.findById(foodTruckId).orElseThrow()); return foodTruckMenuRepository.save(foodTruckMenu); } - FoodTruckMenu updateFoodTruckMenu(Long userId, Long foodTruckId, Long foodTruckMenuId, FoodTruckMenuRequest foodTruckMenuRequest) { + public FoodTruckMenu updateFoodTruckMenu(Long userId, Long foodTruckId, Long foodTruckMenuId, FoodTruckMenuRequest foodTruckMenuRequest) { isManager(userId, foodTruckId); FoodTruckMenu foodTruckMenu = foodTruckMenuRepository.findById(foodTruckMenuId).orElseThrow(); foodTruckMenu.setName(foodTruckMenuRequest.name()); @@ -41,7 +46,7 @@ FoodTruckMenu updateFoodTruckMenu(Long userId, Long foodTruckId, Long foodTruckM return foodTruckMenuRepository.save(foodTruckMenu); } - void deleteFoodTruckMenu(Long userId, Long foodTruckId, Long foodTruckMenuId) { + public void deleteFoodTruckMenu(Long userId, Long foodTruckId, Long foodTruckMenuId) { isManager(userId, foodTruckId); if (!foodTruckMenuRepository.existsById(foodTruckMenuId)) { throw new CustomException(CustomErrorCode.FOOD_TRUCK_MENU_NOT_FOUND); diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckReviewService.java similarity index 67% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewService.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckReviewService.java index 020f3ab..b84dec2 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckReviewService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckReviewService.java @@ -1,5 +1,7 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.service; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckReviewRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java similarity index 89% rename from src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckService.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java index 085f69e..58f045e 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java @@ -1,8 +1,17 @@ -package ac.kr.deu.connect.luck.food_truck; +package ac.kr.deu.connect.luck.food_truck.service; import ac.kr.deu.connect.luck.configuration.MapStructMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckDetailResponse; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckMenuRepository; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckReviewRepository; import ac.kr.deu.connect.luck.user.User; import ac.kr.deu.connect.luck.user.UserRepository; import ac.kr.deu.connect.luck.user.UserRole; diff --git a/src/main/java/ac/kr/deu/connect/luck/now/Now.java b/src/main/java/ac/kr/deu/connect/luck/now/Now.java index 20369ea..244142e 100644 --- a/src/main/java/ac/kr/deu/connect/luck/now/Now.java +++ b/src/main/java/ac/kr/deu/connect/luck/now/Now.java @@ -1,7 +1,7 @@ package ac.kr.deu.connect.luck.now; import ac.kr.deu.connect.luck.common.BaseEntity; -import ac.kr.deu.connect.luck.food_truck.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/ac/kr/deu/connect/luck/now/NowService.java b/src/main/java/ac/kr/deu/connect/luck/now/NowService.java index c81c75f..91fa142 100644 --- a/src/main/java/ac/kr/deu/connect/luck/now/NowService.java +++ b/src/main/java/ac/kr/deu/connect/luck/now/NowService.java @@ -2,8 +2,8 @@ import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; -import ac.kr.deu.connect.luck.food_truck.FoodTruck; -import ac.kr.deu.connect.luck.food_truck.FoodTruckRepository; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; import ac.kr.deu.connect.luck.user.User; import ac.kr.deu.connect.luck.user.UserRepository; import ac.kr.deu.connect.luck.user.UserRole; diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserInfo.java b/src/main/java/ac/kr/deu/connect/luck/user/UserInfo.java index 8e78dcf..47ac667 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserInfo.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserInfo.java @@ -1,6 +1,6 @@ package ac.kr.deu.connect.luck.user; -import ac.kr.deu.connect.luck.food_truck.FoodTruckReview; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; import java.util.List; diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserService.java b/src/main/java/ac/kr/deu/connect/luck/user/UserService.java index 3d6a0a4..dbd7095 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserService.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserService.java @@ -4,7 +4,11 @@ import ac.kr.deu.connect.luck.event.EventRepository; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; -import ac.kr.deu.connect.luck.food_truck.*; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckMenuRepository; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckReviewRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestControllerTest.java b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestControllerTest.java index 460b65d..122a12e 100644 --- a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestControllerTest.java +++ b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMenuRestControllerTest.java @@ -2,6 +2,8 @@ import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomErrorResponse; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.transaction.Transactional; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java index 6a35a80..4407c09 100644 --- a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java +++ b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java @@ -4,6 +4,10 @@ import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomErrorResponse; import ac.kr.deu.connect.luck.exception.CustomException; +import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckRequest; +import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; +import ac.kr.deu.connect.luck.food_truck.entity.FoodType; +import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; import ac.kr.deu.connect.luck.user.User; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; From 452af7fe4a226c3ccf2d4514661df9d9638dd8d6 Mon Sep 17 00:00:00 2001 From: Namju Kim Date: Tue, 16 Apr 2024 03:47:32 +0900 Subject: [PATCH 04/11] refactor: MapStruck -> Entity Mapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 각 클래스별 매퍼클래스 분리(역할 분리) --- .../kr/deu/connect/luck/auth/AuthService.java | 6 ++-- .../deu/connect/luck/event/EventMapper.java | 11 +++++++ .../deu/connect/luck/event/EventService.java | 7 ++--- .../ApplicationStatus.java | 2 +- .../event_application/EventApplication.java | 31 +++++++++++++++++++ .../EventApplicationMapper.java | 3 +- .../EventApplicationRepository.java | 28 +++++++++++++++++ .../EventApplicationRequest.java | 2 +- .../EventApplicationRestController.java | 2 +- .../EventApplicationService.java | 2 +- .../FoodTruckMapper.java} | 18 ++--------- .../service/FoodTruckMenuService.java | 4 +-- .../food_truck/service/FoodTruckService.java | 4 +-- .../kr/deu/connect/luck/user/UserMapper.java | 12 +++++++ .../FoodTruckRestControllerTest.java | 3 +- 15 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 src/main/java/ac/kr/deu/connect/luck/event/EventMapper.java rename src/main/java/ac/kr/deu/connect/luck/{eventApplication => event_application}/ApplicationStatus.java (75%) create mode 100644 src/main/java/ac/kr/deu/connect/luck/event_application/EventApplication.java rename src/main/java/ac/kr/deu/connect/luck/{eventApplication => event_application}/EventApplicationMapper.java (75%) create mode 100644 src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRepository.java rename src/main/java/ac/kr/deu/connect/luck/{eventApplication => event_application}/EventApplicationRequest.java (81%) rename src/main/java/ac/kr/deu/connect/luck/{eventApplication => event_application}/EventApplicationRestController.java (94%) rename src/main/java/ac/kr/deu/connect/luck/{eventApplication => event_application}/EventApplicationService.java (97%) rename src/main/java/ac/kr/deu/connect/luck/{configuration/MapStructMapper.java => food_truck/FoodTruckMapper.java} (65%) create mode 100644 src/main/java/ac/kr/deu/connect/luck/user/UserMapper.java diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java b/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java index 899ae44..67cc778 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java @@ -1,9 +1,9 @@ package ac.kr.deu.connect.luck.auth; -import ac.kr.deu.connect.luck.configuration.MapStructMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; import ac.kr.deu.connect.luck.user.User; +import ac.kr.deu.connect.luck.user.UserMapper; import ac.kr.deu.connect.luck.user.UserRepository; import ac.kr.deu.connect.luck.user.UserRole; import lombok.RequiredArgsConstructor; @@ -16,7 +16,7 @@ @RequiredArgsConstructor public class AuthService { private final UserRepository userRepository; - private final MapStructMapper mapStructMapper; + private final UserMapper userMapper; /** * 회원가입 @@ -27,7 +27,7 @@ public class AuthService { public User signUp(SignUpRequest signUpRequest) { if (userRepository.existsByEmail(signUpRequest.email())) throw new CustomException(CustomErrorCode.ALREADY_EXIST_USER_ID); - User user = mapStructMapper.toUser(signUpRequest); + User user = userMapper.toUser(signUpRequest); user.setRole(UserRole.USER); return userRepository.save(user); } diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventMapper.java b/src/main/java/ac/kr/deu/connect/luck/event/EventMapper.java new file mode 100644 index 0000000..91711af --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventMapper.java @@ -0,0 +1,11 @@ +package ac.kr.deu.connect.luck.event; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface EventMapper { + @Mapping(target = "status", defaultValue = "BEFORE_APPLICATION", ignore = true) + @Mapping(source = "managerId", target = "manager.id") + Event toEvent(EventRequest eventRequest); +} diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventService.java b/src/main/java/ac/kr/deu/connect/luck/event/EventService.java index 94d9fab..24a433c 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event/EventService.java +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventService.java @@ -1,6 +1,5 @@ package ac.kr.deu.connect.luck.event; -import ac.kr.deu.connect.luck.configuration.MapStructMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,7 +9,7 @@ @RequiredArgsConstructor public class EventService { private final EventRepository eventRepository; - private final MapStructMapper mapStructMapper; + private final EventMapper eventMapper; public List getEvents() { return eventRepository.findAll(); @@ -21,7 +20,7 @@ public Event getEvent(Long id) { } public Event createEvent(EventRequest eventRequest) { - return eventRepository.save(mapStructMapper.toEvent(eventRequest)); + return eventRepository.save(eventMapper.toEvent(eventRequest)); } public Event updateEvent(Long id, EventRequest eventRequest) { @@ -39,4 +38,4 @@ public Event updateEvent(Long id, EventRequest eventRequest) { public void deleteEvent(Long id) { eventRepository.deleteById(id); } -} \ No newline at end of file +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java b/src/main/java/ac/kr/deu/connect/luck/event_application/ApplicationStatus.java similarity index 75% rename from src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java rename to src/main/java/ac/kr/deu/connect/luck/event_application/ApplicationStatus.java index fa5a37c..8c242ca 100644 --- a/src/main/java/ac/kr/deu/connect/luck/eventApplication/ApplicationStatus.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/ApplicationStatus.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.eventApplication; +package ac.kr.deu.connect.luck.event_application; public enum ApplicationStatus { PENDING, // 요청 중 diff --git a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplication.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplication.java new file mode 100644 index 0000000..777af71 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplication.java @@ -0,0 +1,31 @@ +package ac.kr.deu.connect.luck.event_application; + +import ac.kr.deu.connect.luck.common.BaseEntity; +import ac.kr.deu.connect.luck.event.Event; +import ac.kr.deu.connect.luck.user.User; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class EventApplication extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + private Event event; + + @ManyToOne + private User foodTruckManager; + + @Enumerated(EnumType.STRING) + private ApplicationStatus status; + + private String comment; + +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java similarity index 75% rename from src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java rename to src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java index 8af290b..f428298 100644 --- a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationMapper.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java @@ -1,6 +1,5 @@ -package ac.kr.deu.connect.luck.eventApplication; +package ac.kr.deu.connect.luck.event_application; -import ac.kr.deu.connect.luck.Application; import org.mapstruct.Mapper; import org.mapstruct.Mapping; diff --git a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRepository.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRepository.java new file mode 100644 index 0000000..2d8df0c --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRepository.java @@ -0,0 +1,28 @@ +package ac.kr.deu.connect.luck.event_application; + +import ac.kr.deu.connect.luck.event.Event; +import ac.kr.deu.connect.luck.user.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface EventApplicationRepository extends JpaRepository { + + /** + * 이벤트 신청자 목록 조회 (행사 관리자용 API) + * + * @param event 조회 하려는 이벤트 + * @return 이벤트에 신청한 신청자 목록 + */ + List findAllByEvent(Event event); + + /** + * 이벤트 신청자 목록 조회 (푸드트럭 매니저용 API) + * + * @param user 조회 하려는 푸드트럭 매니저 + * @return 푸드트럭 매니저가 신청한 이벤트 목록 + */ + List findAllByFoodTruckManager(User user); +} diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRequest.java similarity index 81% rename from src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java rename to src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRequest.java index c23c243..a7428d7 100644 --- a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRequest.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.eventApplication; +package ac.kr.deu.connect.luck.event_application; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRestController.java similarity index 94% rename from src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java rename to src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRestController.java index c19ada4..625a540 100644 --- a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationRestController.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.eventApplication; +package ac.kr.deu.connect.luck.event_application; import lombok.AllArgsConstructor; diff --git a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java similarity index 97% rename from src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java rename to src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java index 14d48ad..67cb752 100644 --- a/src/main/java/ac/kr/deu/connect/luck/eventApplication/EventApplicationService.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java @@ -1,4 +1,4 @@ -package ac.kr.deu.connect.luck.eventApplication; +package ac.kr.deu.connect.luck.event_application; import ac.kr.deu.connect.luck.event.Event; import ac.kr.deu.connect.luck.event.EventRepository; diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMapper.java similarity index 65% rename from src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java rename to src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMapper.java index 2f8a078..63ae585 100644 --- a/src/main/java/ac/kr/deu/connect/luck/configuration/MapStructMapper.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/FoodTruckMapper.java @@ -1,9 +1,6 @@ -package ac.kr.deu.connect.luck.configuration; +package ac.kr.deu.connect.luck.food_truck; + -import ac.kr.deu.connect.luck.auth.LoginRequest; -import ac.kr.deu.connect.luck.auth.SignUpRequest; -import ac.kr.deu.connect.luck.event.Event; -import ac.kr.deu.connect.luck.event.EventRequest; import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckDetailResponse; import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckRequest; @@ -11,22 +8,13 @@ import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckMenu; import ac.kr.deu.connect.luck.food_truck.entity.FoodTruckReview; -import ac.kr.deu.connect.luck.user.User; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import java.util.List; @Mapper(componentModel = "spring") -public interface MapStructMapper { - - User toUser(SignUpRequest signUpRequest); - - User toUser(LoginRequest loginRequest); - - @Mapping(target = "status", defaultValue = "BEFORE_APPLICATION", ignore = true ) - @Mapping(source = "managerId", target = "manager.id") - Event toEvent(EventRequest eventRequest); +public interface FoodTruckMapper { FoodTruck toFoodTruck(FoodTruckRequest foodTruckRequest); diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java index 795e984..64fbecb 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java @@ -1,6 +1,6 @@ package ac.kr.deu.connect.luck.food_truck.service; -import ac.kr.deu.connect.luck.configuration.MapStructMapper; +import ac.kr.deu.connect.luck.food_truck.FoodTruckMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; import ac.kr.deu.connect.luck.food_truck.dto.FoodTruckMenuRequest; @@ -23,7 +23,7 @@ public class FoodTruckMenuService { private final FoodTruckMenuRepository foodTruckMenuRepository; private final FoodTruckRepository foodTruckRepository; private final UserRepository userRepository; - private final MapStructMapper mapStructMapper; + private final FoodTruckMapper mapStructMapper; public List getFoodTruckMenus(Long foodTruckId) { return foodTruckMenuRepository.findByFoodTruckId(foodTruckId); diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java index 58f045e..86f64b9 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java @@ -1,6 +1,6 @@ package ac.kr.deu.connect.luck.food_truck.service; -import ac.kr.deu.connect.luck.configuration.MapStructMapper; +import ac.kr.deu.connect.luck.food_truck.FoodTruckMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; @@ -27,7 +27,7 @@ public class FoodTruckService { private final FoodTruckMenuRepository foodTruckMenuRepository; private final FoodTruckReviewRepository foodTruckReviewRepository; private final UserRepository userRepository; - private final MapStructMapper mapStructMapper; + private final FoodTruckMapper mapStructMapper; /** * 모든 푸드트럭을 조회합니다. diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserMapper.java b/src/main/java/ac/kr/deu/connect/luck/user/UserMapper.java new file mode 100644 index 0000000..8998de4 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserMapper.java @@ -0,0 +1,12 @@ +package ac.kr.deu.connect.luck.user; + +import ac.kr.deu.connect.luck.auth.LoginRequest; +import ac.kr.deu.connect.luck.auth.SignUpRequest; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface UserMapper { + User toUser(SignUpRequest signUpRequest); + + User toUser(LoginRequest loginRequest); +} diff --git a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java index 4407c09..656cf7f 100644 --- a/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java +++ b/src/test/java/ac/kr/deu/connect/luck/food_truck/FoodTruckRestControllerTest.java @@ -1,6 +1,5 @@ package ac.kr.deu.connect.luck.food_truck; -import ac.kr.deu.connect.luck.configuration.MapStructMapper; import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomErrorResponse; import ac.kr.deu.connect.luck.exception.CustomException; @@ -41,7 +40,7 @@ class FoodTruckRestControllerTest { private FoodTruckRepository foodTruckRepository; @Autowired - private MapStructMapper mapStructMapper; + private FoodTruckMapper mapStructMapper; @Test From 6a30ab26cb7355857ca12140554cfe6b7421d73d Mon Sep 17 00:00:00 2001 From: Namju Kim Date: Tue, 16 Apr 2024 14:46:57 +0900 Subject: [PATCH 05/11] Feat: JWT Security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web -> Cookie에 저장 RestAPI -> Header에 필요 --- build.gradle | 7 + .../deu/connect/luck/auth/AuthController.java | 76 ++++++--- .../connect/luck/auth/AuthRestController.java | 13 +- .../kr/deu/connect/luck/auth/AuthService.java | 27 ++- .../deu/connect/luck/auth/LoginRequest.java | 2 +- .../deu/connect/luck/auth/SignUpRequest.java | 2 +- .../luck/configuration/HttpMethodConfig.java | 13 -- .../luck/configuration/SecurityConfig.java | 53 ++++++ .../luck/configuration/SwaggerConfig.java | 22 ++- .../luck/event/EventRestController.java | 4 + .../EventApplicationMapper.java | 3 +- .../EventApplicationService.java | 2 +- .../repository/FoodTruckRepository.java | 1 + .../service/FoodTruckMenuService.java | 12 +- .../food_truck/service/FoodTruckService.java | 2 +- .../deu/connect/luck/now/NowController.java | 16 +- .../kr/deu/connect/luck/now/NowService.java | 74 ++++++--- .../connect/luck/security/JwtTokenFilter.java | 37 +++++ .../luck/security/JwtTokenProvider.java | 155 ++++++++++++++++++ .../connect/luck/security/MyUserDetails.java | 33 ++++ .../ac/kr/deu/connect/luck/user/User.java | 6 +- .../deu/connect/luck/user/UserController.java | 6 +- .../connect/luck/user/UserRestController.java | 8 + .../ac/kr/deu/connect/luck/user/UserRole.java | 11 +- .../kr/deu/connect/luck/user/UserService.java | 21 ++- src/main/resources/application.yaml | 10 ++ src/main/resources/data.sql | 47 ++++-- src/main/resources/templates/auth/login.html | 6 +- .../resources/templates/fragments/header.html | 2 +- .../templates/fragments/nav-bar.html | 19 +-- src/main/resources/templates/index.html | 5 +- .../resources/templates/user/profile.html | 2 +- 32 files changed, 558 insertions(+), 139 deletions(-) delete mode 100644 src/main/java/ac/kr/deu/connect/luck/configuration/HttpMethodConfig.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/configuration/SecurityConfig.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/security/JwtTokenFilter.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/security/JwtTokenProvider.java create mode 100644 src/main/java/ac/kr/deu/connect/luck/security/MyUserDetails.java diff --git a/build.gradle b/build.gradle index 43758e1..16917d1 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0' implementation 'org.commonmark:commonmark:0.21.0' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -37,6 +39,11 @@ dependencies { runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // JWT + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' } tasks.named('test') { diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/AuthController.java b/src/main/java/ac/kr/deu/connect/luck/auth/AuthController.java index 9764ac1..73931a8 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/AuthController.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/AuthController.java @@ -1,21 +1,33 @@ package ac.kr.deu.connect.luck.auth; -import ac.kr.deu.connect.luck.user.User; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; @Controller -@RequiredArgsConstructor +@RequestMapping("/auth") public class AuthController { + @Value("${security.jwt.token.header}") + private String AUTHORIZATION_HEADER; + + @Value("${security.jwt.token.prefix}") + private String TOKEN_PREFIX; + private final AuthService authService; + @Autowired + public AuthController(AuthService authService) { + this.authService = authService; + } + @GetMapping("/login") public String login() { return "auth/login"; @@ -25,15 +37,16 @@ public String login() { public String loginPost( @RequestParam("email") String email, @RequestParam("password") String password, - HttpServletRequest httpServletRequest) { - // 세션 초기화 - httpServletRequest.getSession().invalidate(); - HttpSession session = httpServletRequest.getSession(true); - + HttpServletResponse response) { // 로그인 - User user = authService.login(new LoginRequest(email, password)); - session.setAttribute("user", user); - session.setMaxInactiveInterval(60 * 30); // 30분 + String token = authService.login(new LoginRequest(email, password)); + String encodedToken = URLEncoder.encode(TOKEN_PREFIX + token, StandardCharsets.UTF_8); + + // 쿠키에 토큰 저장 + Cookie cookie = new Cookie(AUTHORIZATION_HEADER, encodedToken); + cookie.setPath("/"); + response.addCookie(cookie); + return "redirect:/"; } @@ -50,10 +63,10 @@ public String signup() { /** * 회원가입 처리 * - * @param email 이메일 - * @param password 비밀번호 - * @param name 이름 - * @param httpServletRequest HttpServletRequest(세션) + * @param email 이메일 + * @param password 비밀번호 + * @param name 이름 + * @param * @return 홈 화면 */ @PostMapping("/signup") @@ -62,18 +75,27 @@ public String signupPost( @RequestParam("password") String password, @RequestParam("name") String name, @RequestParam("phone") String phone, - HttpServletRequest httpServletRequest) { - User user = authService.signUp(new SignUpRequest(email, password, name, phone)); + HttpServletResponse response) { + // 회원가입 후 자동 로그인 + // 쿠키 저장 시 토큰을 URL 인코딩(URLEncoder.encode)하여 저장 + String token = authService.signUp(new SignUpRequest(email, password, name, phone)); + String encodedToken = URLEncoder.encode(TOKEN_PREFIX + token, StandardCharsets.UTF_8); + Cookie cookie = new Cookie(AUTHORIZATION_HEADER, encodedToken); + cookie.setMaxAge(60 * 30); // 30분 + cookie.setPath("/"); + response.addCookie(cookie); - HttpSession session = httpServletRequest.getSession(true); - session.setAttribute("user", user); - session.setMaxInactiveInterval(60 * 30); // 30분 return "redirect:/"; } @GetMapping("/logout") - public String logout(HttpServletRequest httpServletRequest) { - httpServletRequest.getSession().invalidate(); + public String logout(@CookieValue(value = "Authorization", defaultValue = "", required = false) Cookie jwtCookie, + HttpServletResponse httpServletResponse) { + // 쿠키 삭제 + jwtCookie.setMaxAge(0); + jwtCookie.setPath("/"); + jwtCookie.setValue(null); + httpServletResponse.addCookie(jwtCookie); return "redirect:/"; } diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/AuthRestController.java b/src/main/java/ac/kr/deu/connect/luck/auth/AuthRestController.java index 2ba1bb7..b10ae03 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/AuthRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/AuthRestController.java @@ -1,7 +1,6 @@ package ac.kr.deu.connect.luck.auth; import ac.kr.deu.connect.luck.exception.CustomErrorResponse; -import ac.kr.deu.connect.luck.user.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -15,18 +14,18 @@ @Tag(name = "Auth", description = "인증 관련 API") @RestController -@RequestMapping("/api") +@RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthRestController { private final AuthService authService; @PostMapping("/login") - @Operation(summary = "로그인") + @Operation(summary = "로그인", description = "로그인을 수행합니다. 로그인 성공 시 JWT 토큰을 반환합니다.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = User.class))), + @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse(responseCode = "400", description = "로그인 실패", content = @Content(schema = @Schema(implementation = CustomErrorResponse.class))) }) - public User login( + public String login( @Parameter(description = "로그인 요청 정보") @RequestBody LoginRequest loginRequest) { return authService.login(loginRequest); } @@ -34,10 +33,10 @@ public User login( @PostMapping("/signup") @Operation(summary = "회원가입") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = User.class))), + @ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse(responseCode = "400", description = "회원가입 실패", content = @Content(schema = @Schema(implementation = CustomErrorResponse.class))) }) - public User signUp( + public String signUp( @Parameter(name = "로그인 객체", description = "회원가입 요청 정보") @RequestBody SignUpRequest signUpRequest) { return authService.signUp(signUpRequest); } diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java b/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java index 67cc778..7cbecad 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java @@ -2,13 +2,17 @@ import ac.kr.deu.connect.luck.exception.CustomErrorCode; import ac.kr.deu.connect.luck.exception.CustomException; +import ac.kr.deu.connect.luck.security.JwtTokenProvider; import ac.kr.deu.connect.luck.user.User; import ac.kr.deu.connect.luck.user.UserMapper; import ac.kr.deu.connect.luck.user.UserRepository; import ac.kr.deu.connect.luck.user.UserRole; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; @@ -17,19 +21,27 @@ public class AuthService { private final UserRepository userRepository; private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; /** * 회원가입 * * @param signUpRequest 회원가입 요청 정보 - * @return User 엔티티 + * @return String jwt 토큰 */ - public User signUp(SignUpRequest signUpRequest) { + public String signUp(SignUpRequest signUpRequest) { if (userRepository.existsByEmail(signUpRequest.email())) throw new CustomException(CustomErrorCode.ALREADY_EXIST_USER_ID); User user = userMapper.toUser(signUpRequest); - user.setRole(UserRole.USER); - return userRepository.save(user); + List roles = new ArrayList<>(); + roles.add(UserRole.USER); + user.setRoles(roles); + user.setPassword(passwordEncoder.encode(user.getPassword())); + User savedUser = userRepository.save(user); + + System.out.println(savedUser.getPassword()); + return jwtTokenProvider.createToken(savedUser.getEmail(), savedUser.getRoles()); } /** @@ -38,13 +50,14 @@ public User signUp(SignUpRequest signUpRequest) { * @param loginRequest 로그인 요청 정보 * @return User 엔티티 */ - public User login(LoginRequest loginRequest) { + public String login(LoginRequest loginRequest) { Optional user = userRepository.findByEmail(loginRequest.email()); if (user.isEmpty()) throw new CustomException(CustomErrorCode.EMAIL_NOT_FOUND); - if (!user.get().getPassword().equals(loginRequest.password())) throw new CustomException(CustomErrorCode.PASSWORD_NOT_MATCH); + if (!passwordEncoder.matches(loginRequest.password(), user.get().getPassword())) throw new CustomException(CustomErrorCode.PASSWORD_NOT_MATCH); + + return jwtTokenProvider.createToken(user.get().getEmail(), user.get().getRoles()); - return user.get(); } public String findEmailByPhone(String phone) { diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/LoginRequest.java b/src/main/java/ac/kr/deu/connect/luck/auth/LoginRequest.java index 0c77bd4..a05175a 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/LoginRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/LoginRequest.java @@ -6,7 +6,7 @@ public record LoginRequest( @Schema(description = "이메일", example = "test1@test.com") String email, - @Schema(description = "비밀번호", example = "test1") + @Schema(description = "비밀번호", example = "test") String password ) { } diff --git a/src/main/java/ac/kr/deu/connect/luck/auth/SignUpRequest.java b/src/main/java/ac/kr/deu/connect/luck/auth/SignUpRequest.java index 51126a8..8ca6432 100644 --- a/src/main/java/ac/kr/deu/connect/luck/auth/SignUpRequest.java +++ b/src/main/java/ac/kr/deu/connect/luck/auth/SignUpRequest.java @@ -6,7 +6,7 @@ public record SignUpRequest( @Schema(description = "이메일", example = "test@test.com") String email, - @Schema(description = "비밀번호", example = "test1234") + @Schema(description = "비밀번호", example = "test") String password, @Schema(description = "이름", example = "홍길동") String name, diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/HttpMethodConfig.java b/src/main/java/ac/kr/deu/connect/luck/configuration/HttpMethodConfig.java deleted file mode 100644 index 3473820..0000000 --- a/src/main/java/ac/kr/deu/connect/luck/configuration/HttpMethodConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package ac.kr.deu.connect.luck.configuration; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.filter.HiddenHttpMethodFilter; - -@Configuration -public class HttpMethodConfig { - @Bean - public HiddenHttpMethodFilter hiddenHttpMethodFilter() { - return new HiddenHttpMethodFilter(); - } -} diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/SecurityConfig.java b/src/main/java/ac/kr/deu/connect/luck/configuration/SecurityConfig.java new file mode 100644 index 0000000..34b9bec --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/configuration/SecurityConfig.java @@ -0,0 +1,53 @@ +package ac.kr.deu.connect.luck.configuration; + + +import ac.kr.deu.connect.luck.security.JwtTokenFilter; +import ac.kr.deu.connect.luck.security.JwtTokenProvider; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +@AllArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity + // Disable CSRF protection + .csrf(AbstractHttpConfigurer::disable) + // Disable session management + .sessionManagement(sessionManagement -> + sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + // Add JWT token filter + .addFilterBefore( + new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) + // Authorize requests + .authorizeHttpRequests( + authorizeRequests -> authorizeRequests + .anyRequest().permitAll() + ); + + return httpSecurity.build(); + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/configuration/SwaggerConfig.java b/src/main/java/ac/kr/deu/connect/luck/configuration/SwaggerConfig.java index 3e6ef69..597a503 100644 --- a/src/main/java/ac/kr/deu/connect/luck/configuration/SwaggerConfig.java +++ b/src/main/java/ac/kr/deu/connect/luck/configuration/SwaggerConfig.java @@ -1,9 +1,29 @@ package ac.kr.deu.connect.luck.configuration; import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -@OpenAPIDefinition(info = @io.swagger.v3.oas.annotations.info.Info(title = "Connect Luck API", version = "1.0", description = "커넥트 럭을 위한 API")) +@OpenAPIDefinition( + info = @io.swagger.v3.oas.annotations.info.Info(title = "Connect Luck API", version = "1.0", description = "커넥트 럭을 위한 API")) public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI().addSecurityItem(new SecurityRequirement(). + addList("Bearer Authentication")) + .components(new Components().addSecuritySchemes + ("Bearer Authentication", createAPIKeyScheme())); + } + + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } } diff --git a/src/main/java/ac/kr/deu/connect/luck/event/EventRestController.java b/src/main/java/ac/kr/deu/connect/luck/event/EventRestController.java index 39d015c..34235a3 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event/EventRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/event/EventRestController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -29,18 +30,21 @@ public ResponseEntity getEvent(@PathVariable Long id) { } @PatchMapping("/{id}") + @PreAuthorize("hasRole('ROLE_EVENT_MANAGER')") @Operation(summary = "이벤트 수정", description = "이벤트 수정") public ResponseEntity updateEvent(@PathVariable Long id, @RequestBody EventRequest eventRequest) { return ResponseEntity.ok(eventService.updateEvent(id, eventRequest)); } @PostMapping + @PreAuthorize("hasRole('ROLE_EVENT_MANAGER')") @Operation(summary = "이벤트 생성", description = "이벤트 생성\n이벤트 주소입력 시 카카오 우편번호 서비스를 사용해서 주소를 입력받아야함.") public ResponseEntity createEvent(@RequestBody EventRequest eventRequest) { return ResponseEntity.ok(eventService.createEvent(eventRequest)); } @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ROLE_EVENT_MANAGER')") @Operation(summary = "이벤트 삭제", description = "이벤트 삭제") public ResponseEntity deleteEvent(@PathVariable Long id) { eventService.deleteEvent(id); diff --git a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java index f428298..fc36093 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationMapper.java @@ -1,11 +1,10 @@ package ac.kr.deu.connect.luck.event_application; import org.mapstruct.Mapper; -import org.mapstruct.Mapping; @Mapper(componentModel = "spring") public interface EventApplicationMapper { - @Mapping(source = "comment", target = "comment") + EventApplication toEventApplication(EventApplicationRequest eventApplicationRequest); } diff --git a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java index 67cb752..2368173 100644 --- a/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java +++ b/src/main/java/ac/kr/deu/connect/luck/event_application/EventApplicationService.java @@ -36,7 +36,7 @@ public EventApplication createEventApplication(EventApplicationRequest eventAppl // 유저 정보 조회 User user = userRepository.findByEmail(email).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); - if (user.getRole() != UserRole.FOOD_TRUCK_MANAGER){ + if (user.getRoles().contains(UserRole.ADMIN)) { throw new CustomException(CustomErrorCode.USER_ID_NOT_MATCH); } diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java index b8639e3..d35a016 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/repository/FoodTruckRepository.java @@ -19,4 +19,5 @@ public interface FoodTruckRepository extends JpaRepository { List findByNameContainingAndFoodType(String name, FoodType foodType); void deleteByManager(User manager); + } diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java index 64fbecb..40e5b64 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckMenuService.java @@ -61,11 +61,11 @@ protected void isManager(Long userId, Long foodTruckId) { User user = userRepository.findById(userId).orElseThrow( () -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH) ); - if (user.getRole() != UserRole.FOOD_TRUCK_MANAGER) { - throw new CustomException(CustomErrorCode.ROLE_NOT_MATCH); - } - if (!foodTruck.getManager().equals(user)) { - throw new CustomException(CustomErrorCode.FOOD_TRUCK_IS_NOT_YOURS); - } +// if (user.getRoles() != UserRole.FOOD_TRUCK_MANAGER) { +// throw new CustomException(CustomErrorCode.ROLE_NOT_MATCH); +// } +// if (!foodTruck.getManager().equals(user)) { +// throw new CustomException(CustomErrorCode.FOOD_TRUCK_IS_NOT_YOURS); +// } } } diff --git a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java index 86f64b9..b9b00e2 100644 --- a/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java +++ b/src/main/java/ac/kr/deu/connect/luck/food_truck/service/FoodTruckService.java @@ -143,7 +143,7 @@ protected void isManager(Long userId) { User user = userRepository.findById(userId).orElseThrow( () -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH) ); - if (!user.getRole().equals(UserRole.FOOD_TRUCK_MANAGER)) { + if (!user.getRoles().contains(UserRole.FOOD_TRUCK_MANAGER)) { throw new CustomException(CustomErrorCode.ROLE_NOT_MATCH); } } diff --git a/src/main/java/ac/kr/deu/connect/luck/now/NowController.java b/src/main/java/ac/kr/deu/connect/luck/now/NowController.java index e9b5c40..512f5ca 100644 --- a/src/main/java/ac/kr/deu/connect/luck/now/NowController.java +++ b/src/main/java/ac/kr/deu/connect/luck/now/NowController.java @@ -1,10 +1,14 @@ package ac.kr.deu.connect.luck.now; +import ac.kr.deu.connect.luck.security.JwtTokenProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -28,20 +32,22 @@ public ResponseEntity> getNow( @PostMapping("/start/{foodTruckId}") - @Operation(summary = "푸드트럭 운영 시작") + @Operation(summary = "푸드트럭 운영 시작", description = "위도, 경도 입력 시 해당 위치에서 운영을 시작합니다. 푸드트럭 매니저의 권한이 필요합니다.") + @PreAuthorize("hasRole(ROLE_FOOD_TRUCK_MANAGER)") public ResponseEntity startWorking( @PathVariable("foodTruckId") Long foodTruckId, @RequestParam(value = "latitude") Double latitude, @RequestParam(value = "longitude") Double longitude, - @RequestHeader("USER_ID") Long userId) { - return ResponseEntity.ok(nowService.saveStartNow(foodTruckId, userId, latitude, longitude)); + HttpServletRequest req) { + return ResponseEntity.ok(nowService.saveStartNow(foodTruckId, req, latitude, longitude)); } + @PreAuthorize("hasRole(ROLE_FOOD_TRUCK_MANAGER)") @PostMapping("/end/{foodTruckId}") @Operation(summary = "푸드트럭 운영 종료") public ResponseEntity stopWorking( @Parameter(name = "푸드트럭 IDX") @PathVariable("foodTruckId") Long foodTruckId, - @Parameter(name = "유저 IDX") @RequestHeader("USER_ID") Long userId) { - return ResponseEntity.ok(nowService.saveEndNow(foodTruckId, userId)); + HttpServletRequest req) { + return ResponseEntity.ok(nowService.saveEndNow(foodTruckId, req)); } } diff --git a/src/main/java/ac/kr/deu/connect/luck/now/NowService.java b/src/main/java/ac/kr/deu/connect/luck/now/NowService.java index 91fa142..03c2671 100644 --- a/src/main/java/ac/kr/deu/connect/luck/now/NowService.java +++ b/src/main/java/ac/kr/deu/connect/luck/now/NowService.java @@ -4,9 +4,8 @@ import ac.kr.deu.connect.luck.exception.CustomException; import ac.kr.deu.connect.luck.food_truck.entity.FoodTruck; import ac.kr.deu.connect.luck.food_truck.repository.FoodTruckRepository; -import ac.kr.deu.connect.luck.user.User; -import ac.kr.deu.connect.luck.user.UserRepository; -import ac.kr.deu.connect.luck.user.UserRole; +import ac.kr.deu.connect.luck.security.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -17,12 +16,19 @@ public class NowService { private final NowRepository nowRepository; private final FoodTruckRepository foodTruckRepository; - private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; protected static Double DISTANCE = 0.05; + /** + * 현재 운영중인 푸드트럭 조회 + * + * @param latitude 위도 + * @param longitude 경도 + * @return 현재 운영중인 푸드트럭 정보 + */ public List getNow( Double latitude, Double longitude @@ -38,27 +44,57 @@ public List getNow( } } - public Now saveStartNow(Long foodTruckId, Long userId, Double latitude, Double longitude) { - isManager(userId, foodTruckId); + /** + * 푸드트럭 운영 시작 + * + * @param foodTruckId 푸드트럭 ID + * @param req 요청 정보(JWT 토큰) + * @param latitude 위도 + * @param longitude 경도 + * @return 현재 운영중인 푸드트럭 정보 + */ + public Now saveStartNow(Long foodTruckId, HttpServletRequest req, Double latitude, Double longitude) { + // 정보 확인 + String userEmail = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId).orElseThrow( + () -> new CustomException(CustomErrorCode.FOOD_TRUCK_NOT_FOUND) + ); + isManager(userEmail, foodTruck); + // 이미 운영중인지 확인 Now now = nowRepository.findByFoodTruckId(foodTruckId).orElse( Now.builder() .foodTruck(FoodTruck.builder().id(foodTruckId).build()) .build() ); - + // 운영 좌표 저장 now.setLatitude(latitude); now.setLongitude(longitude); now.setIsOperating(true); + // 저장 후 반환 return nowRepository.save(now); } - public Now saveEndNow(Long foodTruckId, Long userId) { - isManager(userId, foodTruckId); + /** + * 푸드트럭 운영 종료 + * + * @param foodTruckId 푸드트럭 ID + * @param req 요청 정보(JWT 토큰) + * @return 현재 운영중인 푸드트럭 정보 + */ + public Now saveEndNow(Long foodTruckId, HttpServletRequest req) { + // 정보 확인 + String userEmail = jwtTokenProvider.getUsername(jwtTokenProvider.resolveToken(req)); + FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId).orElseThrow( + () -> new CustomException(CustomErrorCode.FOOD_TRUCK_NOT_FOUND) + ); + isManager(userEmail, foodTruck); + Now now = nowRepository.findByFoodTruckId(foodTruckId).orElseThrow( () -> new CustomException(CustomErrorCode.FOOD_TRUCK_IS_NOT_OPERATING) ); + if (!now.getIsOperating()) { throw new CustomException(CustomErrorCode.FOOD_TRUCK_IS_NOT_OPERATING); } @@ -66,23 +102,15 @@ public Now saveEndNow(Long foodTruckId, Long userId) { return nowRepository.save(now); } + /** - * Check if the user is the manager of the food truck + * 본인의 푸드트럭인지 확인 * - * @param userId user id - * @param foodTruckId food truck id + * @param userEmail 사용자 이메일 + * @param foodTruck 푸드트럭 */ - protected void isManager(Long userId, Long foodTruckId) { - FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId).orElseThrow( - () -> new CustomException(CustomErrorCode.FOOD_TRUCK_NOT_FOUND) - ); - User user = userRepository.findById(userId).orElseThrow( - () -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH) - ); - if (user.getRole() != UserRole.FOOD_TRUCK_MANAGER) { - throw new CustomException(CustomErrorCode.ROLE_NOT_MATCH); - } - if (foodTruck.getManager().getId() != userId) { + protected void isManager(String userEmail, FoodTruck foodTruck) { + if (!foodTruck.getManager().getEmail().equals(userEmail)) { throw new CustomException(CustomErrorCode.FOOD_TRUCK_IS_NOT_YOURS); } } diff --git a/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenFilter.java b/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenFilter.java new file mode 100644 index 0000000..16f927f --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenFilter.java @@ -0,0 +1,37 @@ +package ac.kr.deu.connect.luck.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (Exception e) { + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(401, e.getMessage()); + return; + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenProvider.java b/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenProvider.java new file mode 100644 index 0000000..ba42698 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/security/JwtTokenProvider.java @@ -0,0 +1,155 @@ +package ac.kr.deu.connect.luck.security; + +import ac.kr.deu.connect.luck.user.UserRole; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class JwtTokenProvider { + @Value("${security.jwt.token.secret}") + private String secretKey; + + @Value("${security.jwt.token.expiration}") + private long validityInMilliseconds; + + @Value("${security.jwt.token.header}") + private String header; + + @Value("${security.jwt.token.prefix}") + private String prefix; + + @Value("${security.jwt.token.issuer}") + private String issuer; + + @Value("${security.jwt.token.audience}") + private String audience; + + private MyUserDetails myUserDetails; + + @Autowired + public JwtTokenProvider(MyUserDetails myUserDetails) { + this.myUserDetails = myUserDetails; + } + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + /** + * 토큰 생성 메서드 + * + * @param username 사용자 이름 + * @param roles 사용자 권한 + * @return 토큰 + */ + public String createToken(String username, List roles) { + Claims claims = Jwts.claims().setSubject(username); + claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).collect(Collectors.toList())); + + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .setIssuer(issuer) + .setAudience(audience) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + /** + * 토큰에서 사용자 이름 추출 메서드 + * + * @param token 토큰 + * @return 사용자 이름 + */ + public String getUsername(String token) { + log.info("getUsername : " + token); + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + + /** + * 토큰에서 인증 정보 추출 메서드 + * + * @param token 토큰 + * @return 인증 정보 + */ + public Authentication getAuthentication(String token) { + log.info("Authentication request : " + token); + UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + /** + * 헤더에서 토큰 추출 메서드 + * + * @param req 요청 + * @return 토큰 + */ + public String resolveToken(HttpServletRequest req) { + // 헤더에서 토큰 추출 + String bearerToken = req.getHeader(header); + if (bearerToken != null && bearerToken.startsWith(prefix)) { + log.info("resolve Token at header : {}", bearerToken); + return bearerToken.substring(7); + } + // 쿠키에서 토큰 추출 + if (req.getCookies() == null) { + log.info("cookie is null"); + return null; + } + for (Cookie cookie : req.getCookies()) { + if (cookie.getName().equals(header)) { + String decodedToken = URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8); + if (decodedToken.startsWith(prefix)) { + log.info("resolve Token at cookie : {}", decodedToken); + return decodedToken.substring(7); + } + } + } + log.info("resolve Token is null"); + return null; + } + + /** + * 토큰 유효성 검사 메서드 + * + * @param token 토큰 + * @return 유효성 여부 + */ + public boolean validateToken(String token) { + log.info("validateToken : " + token); + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + +} diff --git a/src/main/java/ac/kr/deu/connect/luck/security/MyUserDetails.java b/src/main/java/ac/kr/deu/connect/luck/security/MyUserDetails.java new file mode 100644 index 0000000..2b32192 --- /dev/null +++ b/src/main/java/ac/kr/deu/connect/luck/security/MyUserDetails.java @@ -0,0 +1,33 @@ +package ac.kr.deu.connect.luck.security; + +import ac.kr.deu.connect.luck.user.User; +import ac.kr.deu.connect.luck.user.UserRepository; +import lombok.AllArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + + +@Service +@AllArgsConstructor +public class MyUserDetails implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username)); + + return org.springframework.security.core.userdetails.User.builder() + .username(user.getEmail()) + .password(user.getPassword()) + .authorities(user.getRoles()) + .accountExpired(false) + .accountLocked(false) + .credentialsExpired(false) + .disabled(false) + .build(); + } +} diff --git a/src/main/java/ac/kr/deu/connect/luck/user/User.java b/src/main/java/ac/kr/deu/connect/luck/user/User.java index 5f5fd38..c21295b 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/User.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/User.java @@ -4,6 +4,8 @@ import jakarta.persistence.*; import lombok.*; +import java.util.List; + @Entity(name = "users") @Getter @Setter @@ -20,6 +22,8 @@ public class User extends BaseEntity { private String name; @Column(unique = true) private String phone; + + @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) - private UserRole role; + List roles; } diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserController.java b/src/main/java/ac/kr/deu/connect/luck/user/UserController.java index e7fce7f..1365672 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserController.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserController.java @@ -8,6 +8,8 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; +import java.security.Principal; + @Slf4j @Controller @RequiredArgsConstructor @@ -16,8 +18,8 @@ public class UserController { private final UserService userService; @GetMapping - public String gerUserInfo(Model model, HttpServletRequest request) { - User user = (User) request.getSession().getAttribute("user"); + public String gerUserInfo(Principal principal, Model model) { + User user = userService.findUserByEmail(principal.getName()); model.addAttribute("userInfo", userService.findUserInfo(user.getId())); return "user/profile"; } diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserRestController.java b/src/main/java/ac/kr/deu/connect/luck/user/UserRestController.java index ce93dd0..70260cb 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserRestController.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserRestController.java @@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.security.Principal; + @RestController @RequestMapping("/api/user") @AllArgsConstructor @@ -50,4 +53,9 @@ public ResponseEntity setUserRole( ) { return ResponseEntity.ok(userService.setUserRole(id, role)); } + + @GetMapping + public ResponseEntity gerUserInfo(Principal principal) { + return ResponseEntity.ok(userService.findUserByEmail(principal.getName())); + } } diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserRole.java b/src/main/java/ac/kr/deu/connect/luck/user/UserRole.java index 15dd0ec..154af43 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserRole.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserRole.java @@ -1,5 +1,12 @@ package ac.kr.deu.connect.luck.user; -public enum UserRole { - EVENT_MANAGER, USER, ADMIN, FOOD_TRUCK_MANAGER +import org.springframework.security.core.GrantedAuthority; + +public enum UserRole implements GrantedAuthority { + EVENT_MANAGER, USER, ADMIN, FOOD_TRUCK_MANAGER; + + @Override + public String getAuthority() { + return "ROLE_" + this.name(); + } } diff --git a/src/main/java/ac/kr/deu/connect/luck/user/UserService.java b/src/main/java/ac/kr/deu/connect/luck/user/UserService.java index dbd7095..2b69e5a 100644 --- a/src/main/java/ac/kr/deu/connect/luck/user/UserService.java +++ b/src/main/java/ac/kr/deu/connect/luck/user/UserService.java @@ -48,6 +48,10 @@ public User findUserById(Long id) { return userRepository.findById(id).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); } + public User findUserByEmail(String email) { + return userRepository.findByEmail(email).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); + } + public User updateUser(Long id, SignUpRequest user) { User findUser = userRepository.findById(id).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); if (user.email() != null) findUser.setEmail(user.email()); @@ -66,18 +70,13 @@ public UserInfo findUserInfo(Long id) { public User setUserRole(Long id, UserRole role) { User user = userRepository.findById(id).orElseThrow(() -> new CustomException(CustomErrorCode.USER_ID_NOT_MATCH)); - if (user.getRole() == role) { - throw new CustomException(CustomErrorCode.ALREADY_SET_ROLE); - } - // 푸드트럭 매니저는 이벤트 매니저로 변경 불가 - if (user.getRole() == UserRole.FOOD_TRUCK_MANAGER && role == UserRole.EVENT_MANAGER) { - throw new CustomException(CustomErrorCode.ROLE_NOT_BE_CHANGE); - } - // 이벤트 매니저는 푸드트럭 매니저로 변경 불가 - if (user.getRole() == UserRole.EVENT_MANAGER && role == UserRole.FOOD_TRUCK_MANAGER) { - throw new CustomException(CustomErrorCode.ROLE_NOT_BE_CHANGE); + + List roles = user.getRoles(); + + if (!roles.contains(role)) { + roles.add(role); } - user.setRole(role); + return userRepository.save(user); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 5dfd559..8b2463b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -46,3 +46,13 @@ server: imgbb: api-key: ${IMGBB_API_KEY} + +security: + jwt: + token: + secret: ${JWT_SECRET:this-is-a-secret-key-and-it-should-be-kept-secure} + expiration: ${JWT_EXPIRATION:3600000} + header: ${JWT_HEADER:Authorization} + prefix: ${JWT_PREFIX:Bearer } + issuer: ${JWT_ISSUER:connect-luck-server} + audience: ${JWT_AUDIENCE:connect-luck-client} diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4573bbf..579d937 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,15 +1,40 @@ -- User table -INSERT INTO users (created_at, updated_at, email, name, password, role, phone) -VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test1@test.com', 'User1', 'test1', 'USER', '010-1111-1111'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test2@test.com', 'User2', 'test2', 'USER', '010-2222-2222'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test3@test.com', 'User3', 'test3', 'ADMIN', '010-3333-3333'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test4@test.com', 'User4', 'test4', 'EVENT_MANAGER', '010-4444-4444'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test5@test.com', '동의대학교 총학생회', 'test6', 'EVENT_MANAGER', '010-5555-5555'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test6@test.com', 'User6', 'test6', 'FOOD_TRUCK_MANAGER', '010-6666-6666'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test7@test.com', 'User7', 'test7', 'FOOD_TRUCK_MANAGER', '010-7777-7777'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test8@test.com', 'User8', 'test8', 'FOOD_TRUCK_MANAGER', '010-8888-8888'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test9@test.com', 'User9', 'test9', 'FOOD_TRUCK_MANAGER', '010-9999-9999'), - (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test10@test.com', 'User10', 'test10', 'FOOD_TRUCK_MANAGER', '010-1010-1010'); + +INSERT INTO users (created_at, updated_at, email, name, password, phone) +VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test1@test.com', 'User1', '$2a$10$WDkAAQdIbzFjdI1qGiueL.MGxSeI1khstMmVPuT/KEQkUz.RXTZ2u', + '010-1111-1111'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test2@test.com', 'User2', '$2a$10$Z8ehQ.YaREkqcguDP/dX..IL4KyN3il9st.DC5VRcZegobg7JUOo6', + '010-2222-2222'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test3@test.com', 'User3', '$2a$10$OdU9Fx6Q.eZ4ah9Qg4mFRuwAZFRpYuM.i7ebLDwzwd/MhSa1va462', + '010-3333-3333'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test4@test.com', 'User4', '$2a$10$Mms25SF1beM78RfGr1Uh2e9yrGZmNYbVI5TW6tjT4PeAa8SPZVjeG', + '010-4444-4444'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test5@test.com', '동의대학교 총학생회', '$2a$10$OMFRxD2wnt5wBzVZdIlQXegQKFvW53KGj0MgjUni8gaDbufEOg8Ru', + '010-5555-5555'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test6@test.com', 'User6', '$2a$10$IiHU7oYPY6QlJ0BgLbb3ZOQdLyYQrdVEM9zIrOkYV5nUYAEZ5XrdS', + '010-6666-6666'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test7@test.com', 'User7', '$2a$10$v09rUTPHS9.F8ucKTDQOQehllhOS2pAjghnFRCteoC0Hqnr3SsvwS', + '010-7777-7777'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test8@test.com', 'User8', '$2a$10$2FSbkUsqf0VcHLnb0f52P.4zKiI5MfHV90dfN2cYeO2EaATl8QGp', + '010-8888-8888'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test9@test.com', 'User9', '$2a$10$kS8sybs8szmK4AvKx2e1buurVutp6ZUmhMt9vytHLCKx6l9/0h.2G', + '010-9999-9999'), + (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'test10@test.com', 'User10', '$2a$10$Oi98TGDRd4zJhl6PWLGQtufD.D9p967eyFNd3LwSOjYQ89AgC6tFW', + '010-1010-1010'); + +-- user Role +INSERT INTO users_roles (users_id, roles) +VALUES (1, 'USER'), + (2, 'USER'), + (3, 'ADMIN'), + (3, 'USER'), + (4, 'EVENT_MANAGER'), + (5, 'EVENT_MANAGER'), + (6, 'FOOD_TRUCK_MANAGER'), + (7, 'FOOD_TRUCK_MANAGER'), + (8, 'FOOD_TRUCK_MANAGER'), + (9, 'FOOD_TRUCK_MANAGER'), + (10, 'FOOD_TRUCK_MANAGER'); -- Event table diff --git a/src/main/resources/templates/auth/login.html b/src/main/resources/templates/auth/login.html index 8d6cefa..1a4061f 100644 --- a/src/main/resources/templates/auth/login.html +++ b/src/main/resources/templates/auth/login.html @@ -10,7 +10,7 @@

로그인

-
+ diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index c788fce..0e4aca7 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -1,5 +1,5 @@ - + 커넥트럭 diff --git a/src/main/resources/templates/fragments/nav-bar.html b/src/main/resources/templates/fragments/nav-bar.html index e6a21c4..77d9144 100644 --- a/src/main/resources/templates/fragments/nav-bar.html +++ b/src/main/resources/templates/fragments/nav-bar.html @@ -1,5 +1,5 @@ - +
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index a2b6ebf..8e7d14b 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,5 +1,8 @@ - +