├── settings.gradle ├── src ├── test │ ├── http │ │ ├── http-client.env.json │ │ └── study-group.http │ ├── java │ │ └── com │ │ │ └── dsg │ │ │ └── wardstudy │ │ │ ├── WardStudyApplicationTests.java │ │ │ ├── repository │ │ │ ├── user │ │ │ │ ├── UserRepositoryTest.java │ │ │ │ └── UserGroupRepositoryTest.java │ │ │ └── studyGroup │ │ │ │ └── StudyGroupRepositoryTest.java │ │ │ ├── InsertTest.java │ │ │ └── integration │ │ │ └── studyGroup │ │ │ ├── StudyGroupRepositoryIntegrationTest.java │ │ │ └── StudyGroupControllerIntegrationTest.java │ └── resources │ │ └── application-test.yml └── main │ ├── java │ └── com │ │ └── dsg │ │ └── wardstudy │ │ ├── config │ │ ├── redis │ │ │ ├── RedisCacheKey.java │ │ │ ├── RedisSessionConfig.java │ │ │ └── RedisCacheConfig.java │ │ ├── jpa │ │ │ └── JpaAuditingConfig.java │ │ ├── web │ │ │ └── WebConfig.java │ │ ├── swagger │ │ │ └── SwaggerConfig.java │ │ ├── batch │ │ │ ├── BatchJobScheduler.java │ │ │ ├── SimpleJobConfig.java │ │ │ └── NotificationAlarmJobConfig.java │ │ └── file │ │ │ └── FireBaseConfig.java │ │ ├── repository │ │ ├── wish │ │ │ └── WishRepository.java │ │ ├── reservation │ │ │ ├── RoomRepository.java │ │ │ ├── ReservationQueryRepository.java │ │ │ └── ReservationRepository.java │ │ ├── attach │ │ │ └── AttachRepository.java │ │ ├── studyGroup │ │ │ └── StudyGroupRepository.java │ │ ├── user │ │ │ ├── UserRepository.java │ │ │ └── UserGroupRepository.java │ │ └── like │ │ │ └── LikeRepository.java │ │ ├── domain │ │ ├── file │ │ │ ├── FileRepository.java │ │ │ ├── FileResponse.java │ │ │ ├── FileData.java │ │ │ ├── FileController.java │ │ │ └── FileService.java │ │ ├── user │ │ │ ├── constant │ │ │ │ └── UserType.java │ │ │ ├── service │ │ │ │ ├── LoginService.java │ │ │ │ ├── UserService.java │ │ │ │ ├── UserServiceImpl.java │ │ │ │ └── LoginServiceImpl.java │ │ │ ├── dto │ │ │ │ ├── LoginDto.java │ │ │ │ ├── SignUpResponse.java │ │ │ │ ├── UserInfo.java │ │ │ │ └── SignUpRequest.java │ │ │ ├── entity │ │ │ │ ├── UserGroup.java │ │ │ │ └── User.java │ │ │ └── controller │ │ │ │ └── UserController.java │ │ ├── reservation │ │ │ ├── constant │ │ │ │ └── ReservationStatus.java │ │ │ ├── dto │ │ │ │ ├── NotificationAlarmDto.java │ │ │ │ ├── ValidateFindByIdDto.java │ │ │ │ ├── ReservationDetails.java │ │ │ │ └── ReservationCommand.java │ │ │ ├── entity │ │ │ │ ├── Room.java │ │ │ │ └── Reservation.java │ │ │ ├── service │ │ │ │ └── ReservationService.java │ │ │ └── controller │ │ │ │ └── ReservationController.java │ │ ├── comment │ │ │ ├── repository │ │ │ │ └── CommentRepository.java │ │ │ ├── service │ │ │ │ ├── CommentService.java │ │ │ │ └── CommentServiceImpl.java │ │ │ ├── dto │ │ │ │ └── CommentDto.java │ │ │ ├── entity │ │ │ │ └── Comment.java │ │ │ └── controller │ │ │ │ └── CommentController.java │ │ ├── wish │ │ │ ├── service │ │ │ │ ├── WishService.java │ │ │ │ └── WishServiceImpl.java │ │ │ ├── naver │ │ │ │ ├── dto │ │ │ │ │ ├── SearchImageRes.java │ │ │ │ │ ├── SearchLocalReq.java │ │ │ │ │ ├── SearchLocalRes.java │ │ │ │ │ └── SearchImageReq.java │ │ │ │ └── NaverClient.java │ │ │ ├── controller │ │ │ │ └── WishController.java │ │ │ ├── dto │ │ │ │ └── WishDto.java │ │ │ └── entity │ │ │ │ └── Wish.java │ │ ├── BaseTimeEntity.java │ │ ├── attach │ │ │ ├── service │ │ │ │ └── AttachService.java │ │ │ ├── dto │ │ │ │ └── AttachDTO.java │ │ │ ├── entity │ │ │ │ └── Attach.java │ │ │ └── controller │ │ │ │ └── AttachController.java │ │ └── studyGroup │ │ │ ├── service │ │ │ └── StudyGroupService.java │ │ │ ├── dto │ │ │ ├── PageResponse.java │ │ │ ├── StudyGroupRequest.java │ │ │ └── StudyGroupResponse.java │ │ │ ├── entity │ │ │ ├── Like.java │ │ │ └── StudyGroup.java │ │ │ └── controller │ │ │ └── StudyGroupController.java │ │ ├── common │ │ ├── auth │ │ │ ├── AuthUser.java │ │ │ └── AuthUserResolver.java │ │ ├── utils │ │ │ ├── Encryptor.java │ │ │ ├── TokenGenerator.java │ │ │ ├── TimeParsingUtils.java │ │ │ └── FileUtils.java │ │ ├── exception │ │ │ ├── ErrorCode.java │ │ │ ├── ErrorDetails.java │ │ │ ├── ErrorHttpStatusMapper.java │ │ │ ├── WSApiException.java │ │ │ └── WSExceptionHandler.java │ │ ├── adapter │ │ │ ├── mail │ │ │ │ ├── MailController.java │ │ │ │ ├── MailSendService.java │ │ │ │ └── MailMessageGenerator.java │ │ │ └── batch │ │ │ │ └── BatchController.java │ │ └── test │ │ │ └── LogController.java │ │ └── WardStudyApplication.java │ └── resources │ ├── log4j2.xml │ ├── application-dev.yml │ └── application-local.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── Dockerfile ├── docker-compose-redis.yml ├── .gitignore ├── docker-compose.yml ├── .github └── workflows │ └── sonarcloud-analyze.yml ├── gradlew.bat ├── Jenkinsfile ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ward-study' 2 | -------------------------------------------------------------------------------- /src/test/http/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "host": "http://localhost:8081" 4 | } 5 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-lab-edu/ward-study-reservation/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jdk 2 | 3 | LABEL maintainer="mooh2jj" 4 | 5 | ARG JAR_FILE=build/libs/*.jar 6 | 7 | COPY ${JAR_FILE} app.jar 8 | 9 | ENTRYPOINT ["java", \ 10 | "-Dspring.profiles.active=dev", \ 11 | "-jar", \ 12 | "/app.jar"] 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/redis/RedisCacheKey.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.redis; 2 | 3 | public class RedisCacheKey { 4 | public static final String STUDY_GROUP_LIST = "studyGroupList_userId"; 5 | public static final String RESERVATION_LIST = "reservationList_roomId"; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/wish/WishRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.wish; 2 | 3 | import com.dsg.wardstudy.domain.wish.entity.Wish; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface WishRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/reservation/RoomRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.reservation; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Room; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface RoomRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/WardStudyApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class WardStudyApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/jpa/JpaAuditingConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.jpa; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @Configuration 7 | @EnableJpaAuditing 8 | public class JpaAuditingConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/file/FileRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.file; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface FileRepository extends JpaRepository { 8 | 9 | Optional findByName(String fileName); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/constant/UserType.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.constant; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum UserType { 9 | 10 | PARTICIPANT("스터디 참여자"), 11 | LEADER("스터디 리더, 등록자"); 12 | 13 | private final String description; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/constant/ReservationStatus.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.constant; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum ReservationStatus { 9 | 10 | ENABLED("예약 가능"), 11 | CANCELED("예약 취소"); 12 | 13 | private final String description; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/auth/AuthUser.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.PARAMETER}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface AuthUser { 11 | // aop 적용 Target 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.repository; 2 | 3 | import com.dsg.wardstudy.domain.comment.entity.Comment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface CommentRepository extends JpaRepository { 9 | List findByStudyGroupId(Long studyGroupId); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/service/WishService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.service; 2 | 3 | import com.dsg.wardstudy.domain.wish.dto.WishDto; 4 | 5 | import java.util.List; 6 | 7 | public interface WishService { 8 | 9 | WishDto search(String query); 10 | 11 | WishDto add(WishDto wish); 12 | 13 | List getAll(); 14 | 15 | void delete(Long wishId); 16 | 17 | void addVisit(Long wishId); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose-redis.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | redis-cache: 6 | image: redis:latest 7 | container_name: redis-cache 8 | # hostname: redis-cache 9 | command: redis-server --port 6379 10 | ports: 11 | - "6379:6379" 12 | redis-session: 13 | image: redis:latest 14 | container_name: redis-session 15 | # hostname: redis-session 16 | command: redis-server --port 6380 17 | ports: 18 | - "6380:6380" 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/attach/AttachRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.attach; 2 | 3 | import com.dsg.wardstudy.domain.attach.entity.Attach; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface AttachRepository extends JpaRepository { 9 | 10 | void deleteAllByStudyGroupId(Long sgId); 11 | 12 | List findAllByStudyGroupId(Long sgId); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/service/LoginService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.service; 2 | 3 | import com.dsg.wardstudy.domain.user.dto.LoginDto; 4 | import com.dsg.wardstudy.domain.user.dto.SignUpRequest; 5 | import com.dsg.wardstudy.domain.user.dto.SignUpResponse; 6 | 7 | 8 | public interface LoginService { 9 | SignUpResponse signUp(SignUpRequest signUpDto); 10 | 11 | void loginUser(LoginDto loginDto); 12 | 13 | void logoutUser(); 14 | 15 | boolean isLoginUser(); 16 | 17 | Long getUserId(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/utils/Encryptor.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.utils; 2 | 3 | import org.mindrot.jbcrypt.BCrypt; 4 | 5 | public class Encryptor { 6 | 7 | public static String encrypt(String origin) { 8 | return BCrypt.hashpw(origin, BCrypt.gensalt()); 9 | } 10 | 11 | public static boolean isMatch(String origin, String hashed) { 12 | try { 13 | return BCrypt.checkpw(origin, hashed); 14 | } catch (Exception e) { // 여러 예외가 있다. 15 | return false; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/exception/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum ErrorCode { 9 | 10 | NO_FOUND_ENTITY("존재하지 않는 엔티티입니다."), 11 | DUPLICATED_ENTITY("이미 존재하는 엔티티입니다."), 12 | INVALID_REQUEST("요청한 값이 올바르지 않습니다."), 13 | NOT_FOUND_USER("존재하지 않는 사용자입니다."), 14 | INTERNAL_SERVER_ERROR("일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); // 장애 상황 15 | 16 | private final String errorMsg; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/utils/TokenGenerator.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.utils; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | 5 | public class TokenGenerator { 6 | 7 | private static final int TOKEN_LENGTH = 20; 8 | 9 | public static String randomCharacter(int length) { 10 | return RandomStringUtils.randomAlphanumeric(length); 11 | } 12 | 13 | public static String randomCharacterWithPrefix(String prefix) { 14 | return prefix + randomCharacter(TOKEN_LENGTH - prefix.length()); 15 | } 16 | 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/studyGroup/StudyGroupRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.studyGroup; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QuerydslPredicateExecutor; 6 | 7 | import java.util.List; 8 | 9 | public interface StudyGroupRepository extends JpaRepository, QuerydslPredicateExecutor { 10 | 11 | // in 절안에 컬렉션 findByIdIn 12 | List findByIdIn(List ids); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/http/study-group.http: -------------------------------------------------------------------------------- 1 | ### create 2 | POST {{host}}/study-group 3 | Content-Type: application/json 4 | 5 | { 6 | "title" : "dsg2", 7 | "content": "test content02" 8 | } 9 | 10 | 11 | ### getById 12 | GET {{host}}/study-group/2 13 | 14 | 15 | ### getAll 16 | GET {{host}}/study-group 17 | 18 | 19 | ### updateById 20 | PUT {{host}}/study-group/2 21 | Content-Type: application/json 22 | 23 | { 24 | "title" : "dsg_new", 25 | "content": "test_new" 26 | } 27 | 28 | ### deleteById 29 | DELETE {{host}}/study-group/6 30 | 31 | 32 | ### getAllByUserId 33 | GET {{host}}/users/1/study-group 34 | 35 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.service; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 4 | import com.dsg.wardstudy.domain.user.dto.LoginDto; 5 | import com.dsg.wardstudy.domain.user.dto.SignUpRequest; 6 | import com.dsg.wardstudy.domain.user.dto.UserInfo; 7 | 8 | public interface UserService { 9 | 10 | UserInfo create(SignUpRequest signUpRequest); 11 | 12 | UserInfo getUser(Long userId); 13 | 14 | LoginDto getByEmailAndPassword(String email, String password); 15 | 16 | void withdrawUser(Long userId); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/adapter/mail/MailController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.adapter.mail; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | @RequiredArgsConstructor 9 | public class MailController { 10 | private final MailSendService mailSendService; 11 | 12 | @GetMapping("/mail/send") 13 | public void sendMail(String email) { 14 | 15 | mailSendService.sendMail("ehtjd33@gmail.com", "제목입니다.", "테스트입니다."); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.user; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.User; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.Optional; 9 | 10 | public interface UserRepository extends JpaRepository { 11 | Page findBy(Pageable pageable); 12 | 13 | Optional findByEmailAndPassword(String email, String password); 14 | 15 | Optional findByEmail(String email); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/utils/TimeParsingUtils.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.utils; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.format.DateTimeFormatter; 5 | 6 | 7 | public class TimeParsingUtils { 8 | 9 | public static LocalDateTime formatterLocalDateTime(String time) { 10 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 11 | return LocalDateTime.parse(time, formatter); 12 | } 13 | 14 | public static String formatterString(LocalDateTime time) { 15 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 16 | return time.format(formatter); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/like/LikeRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.like; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.Like; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | 8 | import java.util.Optional; 9 | 10 | public interface LikeRepository extends JpaRepository { 11 | Optional findByUserId(Long userId); 12 | 13 | @Query("select COUNT(entity) from Like entity where entity.studyGroup.id = :studyGroupId") 14 | int countByStudyGroupId(@Param("studyGroupId") Long studyGroupId); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.service; 2 | 3 | import com.dsg.wardstudy.domain.comment.dto.CommentDto; 4 | 5 | import java.util.List; 6 | 7 | public interface CommentService { 8 | 9 | CommentDto createComment(Long studyGroupId, Long userId, CommentDto commentDto); 10 | 11 | List getCommentsByStudyGroupId(Long studyGroupId); 12 | 13 | CommentDto getCommentById(Long studyGroupId, Long userId, Long commentId); 14 | 15 | CommentDto updateComment(Long studyGroupId, Long userId, Long commentId, CommentDto commentDto); 16 | 17 | void deleteComment(Long studyGroupId, Long userId, Long commentId); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/WardStudyApplication.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy; 2 | 3 | import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | 9 | @SpringBootApplication 10 | public class WardStudyApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(WardStudyApplication.class, args); 14 | } 15 | 16 | // hibernateLazyInitializer 제거용 라이브러리 17 | @Bean 18 | Hibernate5Module hibernate5Module(){ 19 | return new Hibernate5Module(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### env 40 | .env 41 | 42 | ### fire-base json 43 | file-storage-68596-firebase-adminsdk-5bv5f-260ab6cc65.json 44 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/test/LogController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.test; 2 | 3 | import lombok.extern.log4j.Log4j2; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @Log4j2 8 | @RestController 9 | public class LogController { 10 | 11 | @GetMapping(value = "/log") 12 | public String log() throws Exception { 13 | 14 | //FATAL, ERROR, WARN, INFO, DEBUG, TRACE 15 | log.fatal("FATAL"); 16 | log.error("ERROR"); 17 | log.warn("WARN"); 18 | log.info("INFO"); 19 | log.debug("DEBUG"); 20 | log.trace("TRACE"); 21 | 22 | return "log"; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/exception/ErrorDetails.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.exception; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | @Getter 9 | @NoArgsConstructor 10 | @ToString 11 | public class ErrorDetails { 12 | 13 | private String date; 14 | private String message; 15 | private String description; 16 | private ErrorCode errorCode; 17 | 18 | @Builder 19 | public ErrorDetails(String date, String message, String description, ErrorCode errorCode) { 20 | this.date = date; 21 | this.message = message; 22 | this.description = description; 23 | this.errorCode = errorCode; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/web/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.web; 2 | 3 | import com.dsg.wardstudy.common.auth.AuthUserResolver; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | import java.util.List; 10 | 11 | @Configuration 12 | @RequiredArgsConstructor 13 | public class WebConfig implements WebMvcConfigurer { 14 | 15 | private final AuthUserResolver authResolver; 16 | 17 | @Override 18 | public void addArgumentResolvers(List resolvers) { 19 | resolvers.add(authResolver); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/dto/NotificationAlarmDto.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.dto; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | public class NotificationAlarmDto { 13 | 14 | private String id; 15 | private String email; 16 | private String userName; 17 | private List reservations; 18 | 19 | 20 | 21 | @Builder 22 | public NotificationAlarmDto(String id, String email, String userName, List reservations) { 23 | this.id = id; 24 | this.email = email; 25 | this.userName = userName; 26 | this.reservations = reservations; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/exception/ErrorHttpStatusMapper.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public abstract class ErrorHttpStatusMapper { 6 | 7 | public static HttpStatus mapToStatus(ErrorCode errorCode) { 8 | switch (errorCode) { 9 | case NO_FOUND_ENTITY: 10 | return HttpStatus.NOT_FOUND; 11 | case DUPLICATED_ENTITY: 12 | return HttpStatus.CONFLICT; 13 | case INVALID_REQUEST: 14 | return HttpStatus.BAD_REQUEST; 15 | case NOT_FOUND_USER: 16 | return HttpStatus.UNAUTHORIZED; 17 | // TODO: 나머지는 모두 INTERNAL_SERVER_ERROR 처리 18 | default: 19 | return HttpStatus.INTERNAL_SERVER_ERROR; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/naver/dto/SearchImageRes.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.naver.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class SearchImageRes { 13 | 14 | private String lastBuildDate; 15 | private int total; 16 | private int start; 17 | private int display; 18 | private List items; 19 | 20 | 21 | 22 | @Data 23 | @NoArgsConstructor 24 | @AllArgsConstructor 25 | public static class SearchImageItem { 26 | private String title; 27 | private String link; 28 | private String thumbnail; 29 | private String sizeheight; 30 | private String sizewidth; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | 2 | spring: 3 | config: 4 | activate: 5 | on-profile: test 6 | 7 | datasource: 8 | driver-class-name: com.mysql.cj.jdbc.Driver 9 | url: jdbc:mysql://localhost:3306/ward_study?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 10 | username: ${SPRING_DATASOURCE_USERNAME} 11 | password: ${SPRING_DATASOURCE_PASSWORD} 12 | 13 | jpa: 14 | open-in-view: true 15 | hibernate: 16 | ddl-auto: update 17 | naming: 18 | physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 19 | use-new-id-generator-mappings: false 20 | show-sql: true 21 | properties: 22 | hibernate.format_sql: true 23 | dialect: org.hibernate.dialect.MySQL8Dialect 24 | 25 | logging: 26 | level: 27 | org.hibernate.SQL: debug 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/file/FileResponse.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.file; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Builder 12 | public class FileResponse { 13 | private String fileName; 14 | private String downloadUri; 15 | 16 | private String type; 17 | private long size; 18 | private byte[] imageData; 19 | 20 | public static FileResponse of(FileData fileData) { 21 | return FileResponse.builder() 22 | .fileName(fileData.getName()) 23 | .type(fileData.getType()) 24 | .size(fileData.getSize()) 25 | .downloadUri(fileData.getFilePath()) 26 | .imageData(fileData.getImageData()) 27 | .build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/dto/ValidateFindByIdDto.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.dto; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import com.dsg.wardstudy.domain.reservation.entity.Room; 5 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 6 | import com.dsg.wardstudy.domain.user.entity.User; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | 10 | @Data 11 | public class ValidateFindByIdDto { 12 | 13 | private User user; 14 | private StudyGroup studyGroup; 15 | private Room room; 16 | private Reservation reservation; 17 | 18 | @Builder 19 | public ValidateFindByIdDto(User user, StudyGroup studyGroup, Room room, Reservation reservation) { 20 | this.user = user; 21 | this.studyGroup = studyGroup; 22 | this.room = room; 23 | this.reservation = reservation; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/BaseTimeEntity.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Getter; 5 | import org.springframework.data.annotation.CreatedDate; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.Column; 10 | import javax.persistence.EntityListeners; 11 | import javax.persistence.MappedSuperclass; 12 | import java.time.LocalDateTime; 13 | 14 | @Getter 15 | @MappedSuperclass 16 | @EntityListeners(AuditingEntityListener.class) 17 | public abstract class BaseTimeEntity { 18 | 19 | @CreatedDate 20 | @JsonIgnore 21 | @Column(name = "create_date") 22 | private LocalDateTime createdDate; 23 | 24 | @LastModifiedDate 25 | @JsonIgnore 26 | @Column(name = "update_date") 27 | private LocalDateTime modifiedDate; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/naver/dto/SearchLocalReq.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.naver.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.util.LinkedMultiValueMap; 7 | import org.springframework.util.MultiValueMap; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class SearchLocalReq { 13 | 14 | private String query = ""; 15 | 16 | private int display = 1; 17 | 18 | private int start = 1; 19 | 20 | private String sort = "random"; 21 | 22 | public MultiValueMap toMultiValueMap() { 23 | LinkedMultiValueMap map = new LinkedMultiValueMap<>(); 24 | 25 | map.add("query", query); 26 | map.add("display", String.valueOf(display)); 27 | map.add("start", String.valueOf(start)); 28 | map.add("sort", sort); 29 | 30 | return map; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/attach/service/AttachService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.attach.service; 2 | 3 | import com.dsg.wardstudy.domain.attach.dto.AttachDTO; 4 | import com.dsg.wardstudy.repository.attach.AttachRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.log4j.Log4j2; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Log4j2 13 | @RequiredArgsConstructor 14 | @Service 15 | public class AttachService { 16 | 17 | private final AttachRepository attachRepository; 18 | 19 | public List getAttachList(Long sgId) { 20 | List attachDTOS = attachRepository.findAllByStudyGroupId(sgId).stream() 21 | .map(AttachDTO::toDTO) 22 | .collect(Collectors.toList()); 23 | log.info("service getAttachList, attachDTOS: {}", attachDTOS); 24 | return attachDTOS; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/naver/dto/SearchLocalRes.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.naver.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class SearchLocalRes { 13 | 14 | private String lastBuildDate = ""; 15 | private int total; 16 | private int start; 17 | private int display; 18 | private List items; 19 | 20 | 21 | @Data 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | public static class SearchLocalItem { 25 | private String title; 26 | private String link; 27 | private String category; 28 | private String description; 29 | private String telephone; 30 | private String address; 31 | private String roadAddress; 32 | private int mapx; 33 | private int mapy; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/naver/dto/SearchImageReq.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.naver.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.util.LinkedMultiValueMap; 7 | import org.springframework.util.MultiValueMap; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class SearchImageReq { 13 | 14 | private String query = ""; 15 | 16 | private int display = 1; 17 | 18 | private int start = 1; 19 | 20 | private String sort = "sim"; 21 | 22 | private String filter = "all"; 23 | 24 | public MultiValueMap toMultiValueMap(){ 25 | var map = new LinkedMultiValueMap(); 26 | 27 | map.add("query",query); 28 | map.add("display",String.valueOf(display)); 29 | map.add("start", String.valueOf(start)); 30 | map.add("sort",sort); 31 | map.add("filter",filter); 32 | return map; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/file/FileData.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.file; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @Entity 13 | @Table(name = "file_data") 14 | public class FileData { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private Long id; 19 | 20 | private String name; 21 | 22 | private String type; 23 | 24 | private long size; 25 | 26 | private String filePath; 27 | 28 | @Lob 29 | @Column(name = "imagedata", length = 1000) 30 | private byte[] imageData; 31 | 32 | @Builder 33 | public FileData(String name, String type, long size, String filePath, byte[] imageData) { 34 | this.name = name; 35 | this.type = type; 36 | this.size = size; 37 | this.filePath = filePath; 38 | this.imageData = imageData; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/entity/Room.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import lombok.AccessLevel; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.persistence.*; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Getter 15 | @Entity 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Table(name ="room") 18 | public class Room extends BaseTimeEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "room_id") 23 | private Long id; 24 | 25 | private String name; 26 | 27 | @OneToMany(mappedBy = "room", cascade = CascadeType.ALL) 28 | @JsonIgnore 29 | private List reservations = new ArrayList<>(); 30 | 31 | @Builder 32 | public Room(Long id, String name) { 33 | this.id = id; 34 | this.name = name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/dto/LoginDto.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.dto; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.User; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.validation.constraints.Email; 9 | import javax.validation.constraints.NotBlank; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | public class LoginDto { 14 | 15 | private Long id; 16 | private String name; 17 | 18 | @Email 19 | private String email; 20 | @NotBlank 21 | private String password; 22 | 23 | @Builder 24 | public LoginDto(Long id, String name, String email, String password) { 25 | this.id = id; 26 | this.name = name; 27 | this.email = email; 28 | this.password = password; 29 | } 30 | 31 | public static LoginDto mapToDto(User user) { 32 | return LoginDto.builder() 33 | .id(user.getId()) 34 | .email(user.getEmail()) 35 | .name(user.getName()) 36 | .password(user.getPassword()) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/dto/SignUpResponse.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @NoArgsConstructor 9 | public class SignUpResponse { 10 | 11 | private Long id; 12 | private String name; 13 | private String nickName; 14 | 15 | private String email; 16 | private String password; 17 | 18 | @Builder 19 | public SignUpResponse(Long id, String name, String nickName, String email, String password) { 20 | this.id = id; 21 | this.name = name; 22 | this.nickName = nickName; 23 | this.email = email; 24 | this.password = password; 25 | } 26 | 27 | public static SignUpResponse mapToDto(UserInfo userInfo) { 28 | return SignUpResponse.builder() 29 | .id(userInfo.getId()) 30 | .email(userInfo.getEmail()) 31 | .name(userInfo.getName()) 32 | .nickName(userInfo.getNickName()) 33 | .password(userInfo.getPassword()) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/dto/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.dto; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.User; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class UserInfo { 11 | 12 | private Long id; 13 | private String name; 14 | private String nickName; 15 | 16 | private String email; 17 | private String password; 18 | 19 | @Builder 20 | public UserInfo(Long id, String name, String nickName, String email, String password) { 21 | this.id = id; 22 | this.name = name; 23 | this.nickName = nickName; 24 | this.email = email; 25 | this.password = password; 26 | } 27 | 28 | public static UserInfo mapToDto(User savedUser) { 29 | return UserInfo.builder() 30 | .id(savedUser.getId()) 31 | .email(savedUser.getEmail()) 32 | .name(savedUser.getName()) 33 | .nickName(savedUser.getNickname()) 34 | .password(savedUser.getPassword()) 35 | .build(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/service/ReservationService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.service; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import com.dsg.wardstudy.domain.reservation.dto.ReservationCommand; 5 | import com.dsg.wardstudy.domain.reservation.dto.ReservationDetails; 6 | 7 | import java.util.List; 8 | 9 | public interface ReservationService { 10 | 11 | ReservationDetails register(Long studyGroupId, Long roomId, ReservationCommand.RegisterReservation registerReservation); 12 | 13 | List getAllByUserId(Long userId); 14 | 15 | List getByRoomIdAndTimePeriod(Long roomId, String startTime, String endTime); 16 | 17 | List getByRoomId(Long roomId); 18 | 19 | ReservationDetails getByRoomIdAndReservationToken(Long roomId, String reservationToken); 20 | 21 | String updateByToken(Long roomId, String reservationToken, ReservationCommand.UpdateReservation updateReservation); 22 | 23 | void deleteByToken(Long userId, String reservationToken); 24 | 25 | void changeIsEmailSent(Reservation reservation); 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/service/StudyGroupService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.service; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.dto.PageResponse; 4 | import com.dsg.wardstudy.domain.studyGroup.dto.StudyGroupRequest; 5 | import com.dsg.wardstudy.domain.studyGroup.dto.StudyGroupResponse; 6 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 7 | import org.springframework.data.domain.Pageable; 8 | 9 | import java.util.List; 10 | 11 | public interface StudyGroupService { 12 | 13 | StudyGroupResponse register(Long userId, StudyGroupRequest studyGroupRequest); 14 | 15 | StudyGroupResponse getById(Long studyGroupId); 16 | 17 | PageResponse.StudyGroupDetail getAll(Pageable pageable, String type, String keyword); 18 | 19 | Long updateById(Long userId, Long studyGroupId, StudyGroupRequest studyGroupRequest); 20 | 21 | void deleteById(Long userId, Long studyGroupId); 22 | 23 | List getAllByUserId(Long userId); 24 | 25 | UserGroup participate(Long userId, Long studyGroupId); 26 | 27 | void like(Long userId, Long studyGroupId); 28 | 29 | int likeCount(Long studyGroupId); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/attach/dto/AttachDTO.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.attach.dto; 2 | 3 | import com.dsg.wardstudy.domain.attach.entity.Attach; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class AttachDTO { 11 | 12 | private String uuid; 13 | private String uploadPath; 14 | private String fileName; 15 | private boolean image; 16 | 17 | private Long sgId; 18 | 19 | @Builder 20 | public AttachDTO(String uuid, String uploadPath, String fileName, boolean image, Long sgId) { 21 | this.uuid = uuid; 22 | this.uploadPath = uploadPath; 23 | this.fileName = fileName; 24 | this.image = image; 25 | this.sgId = sgId; 26 | } 27 | 28 | public static AttachDTO toDTO(Attach attach) { 29 | return AttachDTO.builder() 30 | .uuid(attach.getUuid()) 31 | .uploadPath(attach.getUploadPath()) 32 | .fileName(attach.getFileName()) 33 | .image(attach.isImage()) 34 | .sgId(attach.getStudyGroup().getId()) 35 | .build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/dto/CommentDto.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.dto; 2 | 3 | import com.dsg.wardstudy.domain.comment.entity.Comment; 4 | import com.dsg.wardstudy.domain.user.entity.User; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | public class CommentDto { 12 | 13 | private Long id; 14 | 15 | private String name; 16 | 17 | private String email; 18 | 19 | private String body; 20 | 21 | @Builder 22 | public CommentDto(Long id, String name, String email, String body) { 23 | this.id = id; 24 | this.name = name; 25 | this.email = email; 26 | this.body = body; 27 | } 28 | 29 | public static CommentDto mapToDto(Comment savedComment) { 30 | return CommentDto.builder() 31 | .id(savedComment.getId()) 32 | .name(savedComment.getUser().getName()) 33 | .email(savedComment.getUser().getEmail()) 34 | .body(savedComment.getBody()) 35 | .build(); 36 | } 37 | 38 | public void crateUserInfo(User user) { 39 | this.name = user.getName(); 40 | this.email = user.getEmail(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/swagger/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.swagger; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.ApiInfoBuilder; 6 | import springfox.documentation.builders.PathSelectors; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.service.ApiInfo; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | 12 | @Configuration 13 | public class SwaggerConfig { 14 | 15 | @Bean 16 | public Docket api() { 17 | return new Docket(DocumentationType.OAS_30) 18 | .useDefaultResponseMessages(false) 19 | .select() 20 | .apis(RequestHandlerSelectors.basePackage("com.dsg.wardstudy")) 21 | .paths(PathSelectors.any()) 22 | .build() 23 | .apiInfo(apiInfo()); 24 | } 25 | 26 | private ApiInfo apiInfo() { 27 | return new ApiInfoBuilder() 28 | .title("swagger-api-Docs") 29 | .description("SwaggerConfig") 30 | .version("3.0") 31 | .build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/dto/PageResponse.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.dto; 2 | 3 | import lombok.*; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | 7 | import java.util.List; 8 | 9 | 10 | public class PageResponse { 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Builder 16 | public static class StudyGroupDetail { 17 | private List content; 18 | private int pageNo; 19 | private int pageSize; 20 | private long totalElements; 21 | private int totalPages; 22 | private boolean last; 23 | } 24 | 25 | public static StudyGroupDetail of(Pageable pageable, Page studyGroupResponsePage) { 26 | return PageResponse.StudyGroupDetail.builder() 27 | .content(studyGroupResponsePage.getContent()) 28 | .pageNo(pageable.getPageNumber()) 29 | .pageSize(pageable.getPageSize()) 30 | .totalElements(studyGroupResponsePage.getTotalElements()) 31 | .totalPages(studyGroupResponsePage.getTotalPages()) 32 | .last(studyGroupResponsePage.isLast()) 33 | .build(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/dto/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.Pattern; 9 | import javax.validation.constraints.Size; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | public class SignUpRequest { 14 | 15 | @NotBlank 16 | @Size(min = 2, max = 12, message = "이름은 2글자 이상 12글자 이하여야 합니다.") 17 | private String name; 18 | 19 | @NotBlank 20 | @Pattern(regexp = "[a-zA-z0-9]+@[a-zA-z]+[.]+[a-zA-z.]+", message= "올바른 이메일 형식이어야 합니다.") 21 | private String email; 22 | 23 | @NotBlank 24 | @Size(min = 2, max = 16, message = "닉네임은 2글자 이상 16글자 이하여야 합니다.") 25 | private String nickname; 26 | 27 | @NotBlank 28 | @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{4,15}$", 29 | message= "비밀번호는 4글자 이상, 16글자 미만 그리고 영문/숫자/특수문자 포함이어야 합니다.") 30 | private String password; 31 | 32 | @Builder 33 | public SignUpRequest(String name, String email, String nickname, String password) { 34 | this.name = name; 35 | this.email = email; 36 | this.nickname = nickname; 37 | this.password = password; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/entity/Like.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.user.entity.User; 5 | import lombok.*; 6 | 7 | import javax.persistence.*; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @Entity 12 | @Table(name = "likes") 13 | public class Like extends BaseTimeEntity { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | @Column(name = "like_id", nullable = false) 18 | private Long id; 19 | 20 | @ManyToOne(fetch = FetchType.LAZY) 21 | @JoinColumn(name = "study_group_id", nullable = false) 22 | @Setter 23 | private StudyGroup studyGroup; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | @JoinColumn(name = "user_id") 27 | @Setter 28 | private User user; 29 | 30 | @Builder 31 | public Like(Long id, User user, StudyGroup studyGroup) { 32 | this.id = id; 33 | this.user = user; 34 | this.studyGroup = studyGroup; 35 | } 36 | 37 | public static Like of(User user, StudyGroup studyGroup) { 38 | return Like.builder() 39 | .user(user) 40 | .studyGroup(studyGroup) 41 | .build(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/batch/BatchJobScheduler.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.batch; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.batch.core.Job; 6 | import org.springframework.batch.core.JobParameter; 7 | import org.springframework.batch.core.JobParameters; 8 | import org.springframework.batch.core.launch.JobLauncher; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Log4j2 17 | @Component 18 | @EnableScheduling 19 | @RequiredArgsConstructor 20 | public class BatchJobScheduler { 21 | 22 | private final JobLauncher jobLauncher; 23 | 24 | private final Job notificationAlarmJob; 25 | 26 | @Scheduled(cron = "0 0/30 * * * *") 27 | public void runJob() { 28 | 29 | Map parameters = new HashMap<>(); 30 | parameters.put("timestamp", new JobParameter(System.currentTimeMillis())); 31 | 32 | try { 33 | jobLauncher.run(notificationAlarmJob, new JobParameters(parameters)); 34 | } catch (Exception e){ 35 | log.error(e.getMessage()); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/controller/WishController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.controller; 2 | 3 | import com.dsg.wardstudy.domain.wish.dto.WishDto; 4 | import com.dsg.wardstudy.domain.wish.service.WishService; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | 12 | @Slf4j 13 | @RestController 14 | @RequiredArgsConstructor 15 | public class WishController { 16 | 17 | private final WishService wishService; 18 | 19 | @GetMapping("/wish/search") 20 | public WishDto search(@RequestParam String query) { 21 | return wishService.search(query); 22 | } 23 | 24 | @PostMapping("/wish") 25 | public WishDto add(@RequestBody WishDto wishDto){ 26 | log.info("{}", wishDto); 27 | return wishService.add(wishDto); 28 | } 29 | 30 | @GetMapping("/wish") 31 | public List getAll() { 32 | return wishService.getAll(); 33 | } 34 | 35 | @DeleteMapping("/wish/{wishId}") 36 | public void delete(@PathVariable long wishId){ 37 | wishService.delete(wishId); 38 | } 39 | 40 | @PostMapping("/wish/{wishId}") 41 | public void addVisit(@PathVariable long wishId) { 42 | wishService.addVisit(wishId); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/file/FireBaseConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.file; 2 | 3 | import com.google.auth.oauth2.GoogleCredentials; 4 | import com.google.firebase.FirebaseApp; 5 | import com.google.firebase.FirebaseOptions; 6 | import lombok.extern.log4j.Log4j2; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.io.ClassPathResource; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.io.IOException; 13 | 14 | @Log4j2 15 | @Configuration 16 | public class FireBaseConfig { 17 | @Value("${app.firebase-configuration-file}") 18 | private String firebaseConfigPath; 19 | 20 | @PostConstruct 21 | public void initialize() { 22 | try { 23 | FirebaseOptions options = new FirebaseOptions.Builder().setCredentials( 24 | GoogleCredentials.fromStream( 25 | new ClassPathResource(firebaseConfigPath).getInputStream())).build(); 26 | if (FirebaseApp.getApps().isEmpty()) { 27 | FirebaseApp.initializeApp(options); 28 | log.info("Firebase application has been initialized"); 29 | } 30 | } catch (IOException e) { 31 | log.error(e.getMessage()); 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/repository/user/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.user; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.User; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 8 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.data.domain.Sort; 13 | 14 | @Slf4j 15 | @DataJpaTest 16 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 17 | class UserRepositoryTest { 18 | 19 | @Autowired 20 | private UserRepository userRepository; 21 | 22 | 23 | @Test 24 | public void givenPageInfo_whenFindBy_thenPageList(){ 25 | // given - precondition or setup 26 | Pageable pageable = PageRequest.of(0, 10, Sort.by("id").ascending()); 27 | 28 | // when - action or the behaviour that we are going test 29 | Page userPages = userRepository.findBy(pageable); 30 | // then - verify the output 31 | userPages.get().forEach(System.out::println); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/batch/SimpleJobConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.batch; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.batch.core.Job; 6 | import org.springframework.batch.core.Step; 7 | import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; 8 | import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; 9 | import org.springframework.batch.repeat.RepeatStatus; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Log4j2 14 | @RequiredArgsConstructor 15 | @Configuration 16 | public class SimpleJobConfig { 17 | 18 | private final JobBuilderFactory jobBuilderFactory; 19 | private final StepBuilderFactory stepBuilderFactory; 20 | 21 | @Bean 22 | public Job simpleJob() { 23 | return jobBuilderFactory.get("simpleJob") 24 | .start(simpleStep1()) 25 | .build(); 26 | } 27 | 28 | @Bean 29 | public Step simpleStep1() { 30 | return stepBuilderFactory.get("simpleStep1") 31 | .tasklet((contribution, chunkContext) -> { 32 | log.info(">>>>> This is Step1"); 33 | return RepeatStatus.FINISHED; 34 | }) 35 | .build(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/auth/AuthUserResolver.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.auth; 2 | 3 | import com.dsg.wardstudy.domain.user.service.LoginService; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.MethodParameter; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.bind.support.WebDataBinderFactory; 9 | import org.springframework.web.context.request.NativeWebRequest; 10 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 11 | import org.springframework.web.method.support.ModelAndViewContainer; 12 | 13 | 14 | @Slf4j 15 | @Component 16 | @RequiredArgsConstructor 17 | public class AuthUserResolver implements HandlerMethodArgumentResolver { 18 | 19 | private final LoginService loginService; 20 | 21 | @Override 22 | public boolean supportsParameter(MethodParameter parameter) { 23 | return parameter.hasParameterAnnotation(AuthUser.class); 24 | } 25 | 26 | @Override 27 | public Object resolveArgument( 28 | MethodParameter parameter, 29 | ModelAndViewContainer mavContainer, 30 | NativeWebRequest webRequest, 31 | WebDataBinderFactory binderFactory) throws Exception { 32 | 33 | log.info("AuthUserResolver resolveArgument run............."); 34 | return loginService.getUserId(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/user/UserGroupRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.user; 2 | 3 | import com.dsg.wardstudy.domain.user.entity.User; 4 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 5 | import com.dsg.wardstudy.domain.user.constant.UserType; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | public interface UserGroupRepository extends JpaRepository { 14 | 15 | @Query("select ug from UserGroup ug left join fetch ug.user where ug.user.id = :userId") 16 | List findByUserId(@Param("userId") Long userId); 17 | 18 | @Query("select ug from UserGroup ug where ug.user.id = :userId and ug.studyGroup.id = :sgId") 19 | Optional findByUserIdAndSGId(@Param("userId") Long userId, @Param("sgId") Long sgId); 20 | 21 | @Query("select ug.userType from UserGroup ug where ug.user.id = :userId and ug.studyGroup.id = :sgId") 22 | Optional findUserTypeByUserIdAndSGId(@Param("userId") Long userId, @Param("sgId") Long sgId); 23 | 24 | @Query("select ug.studyGroup.id from UserGroup ug where ug.user.id = :userId") 25 | List findSgIdsByUserId(@Param("userId") Long userId); 26 | 27 | @Query("select ug.user from UserGroup ug where ug.studyGroup.id = :sgId") 28 | List findUserBySGId(@Param("sgId") Long sgId); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/adapter/batch/BatchController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.adapter.batch; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.JobExecution; 6 | import org.springframework.batch.core.JobParameter; 7 | import org.springframework.batch.core.JobParameters; 8 | import org.springframework.batch.core.launch.JobLauncher; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | 17 | @RestController 18 | @RequestMapping("/batch") 19 | @RequiredArgsConstructor 20 | public class BatchController { 21 | 22 | private final JobLauncher jobLauncher; 23 | private final Job notificationAlarmJob; 24 | 25 | @GetMapping("/job") 26 | public String startJob() throws Exception { 27 | System.out.println("Starting the batch job"); 28 | System.out.println("job: " +notificationAlarmJob); 29 | 30 | Map parameters = new HashMap<>(); 31 | parameters.put("timestamp", new JobParameter(System.currentTimeMillis())); 32 | JobExecution jobExecution = jobLauncher.run(notificationAlarmJob, new JobParameters(parameters)); 33 | System.out.println("Batch job "+ jobExecution.getStatus()); 34 | 35 | return "Batch job "+ jobExecution.getStatus(); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/adapter/mail/MailSendService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.adapter.mail; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.mail.javamail.JavaMailSender; 6 | import org.springframework.mail.javamail.MimeMessageHelper; 7 | import org.springframework.mail.javamail.MimeMessagePreparator; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.mail.internet.MimeMessage; 11 | 12 | @Log4j2 13 | @Component 14 | @RequiredArgsConstructor 15 | public class MailSendService { 16 | 17 | private final JavaMailSender javaMailSender; 18 | 19 | public boolean sendMail(String mail, String subject, String text) { 20 | 21 | boolean result = false; 22 | 23 | MimeMessagePreparator msg = new MimeMessagePreparator() { 24 | @Override 25 | public void prepare(MimeMessage mimeMessage) throws Exception { 26 | MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); 27 | mimeMessageHelper.setTo(mail); 28 | mimeMessageHelper.setSubject(subject); 29 | mimeMessageHelper.setText(text, true); 30 | } 31 | }; 32 | try { 33 | javaMailSender.send(msg); 34 | log.info("javaMailSender.send"); 35 | result = true; 36 | 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | } 40 | 41 | return result; 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/entity/UserGroup.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 5 | import com.dsg.wardstudy.domain.user.constant.UserType; 6 | import lombok.*; 7 | 8 | import javax.persistence.*; 9 | 10 | @Getter 11 | @Entity 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @Table(name = "user_group") 14 | @ToString(of = {"id", "userType", "study_group_id", "user_id"}) 15 | public class UserGroup extends BaseTimeEntity { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "user_group_id") 20 | private Long id; 21 | 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | @JoinColumn(name = "user_id") 24 | private User user; 25 | 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | @JoinColumn(name = "study_group_id") 28 | private StudyGroup studyGroup; 29 | 30 | 31 | @Enumerated(EnumType.STRING) 32 | @Column(name = "user_type") 33 | private UserType userType; 34 | 35 | @Builder 36 | public UserGroup(Long id, User user, StudyGroup studyGroup, UserType userType) { 37 | this.id = id; 38 | this.user = user; 39 | this.studyGroup = studyGroup; 40 | this.userType = userType; 41 | } 42 | 43 | public void setStudyGroup(StudyGroup studyGroup) { 44 | this.studyGroup = studyGroup; 45 | } 46 | 47 | public void setUser(User user) { 48 | this.user = user; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/file/FileController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.file; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import java.io.IOException; 12 | 13 | @Log4j2 14 | @RestController 15 | @RequiredArgsConstructor 16 | @RequestMapping("/file") 17 | public class FileController { 18 | 19 | final private FileService storageService; 20 | 21 | // 업로드 22 | @PostMapping("/upload") 23 | public ResponseEntity upload(@RequestParam("file") MultipartFile file) throws IOException { 24 | log.info("file upload : {}", file); 25 | return ResponseEntity.status(HttpStatus.OK).body(storageService.uploadImageToFileSystem(file)); 26 | } 27 | 28 | // 다운로드 29 | @GetMapping("/download/{fileCodeName}") 30 | public ResponseEntity downloadImage(@PathVariable("fileCodeName") String fileCodeName) throws IOException { 31 | log.info("file download fileCodeName: {}", fileCodeName); 32 | // byte[] downloadImage = storageService.downloadImage(fileName); 33 | byte[] downloadImage = storageService.downloadImageFromFileSystem(fileCodeName); 34 | 35 | return ResponseEntity.status(HttpStatus.OK) 36 | .contentType(MediaType.valueOf("image/png")) 37 | .body(downloadImage); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/dto/StudyGroupRequest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.dto; 2 | 3 | import com.dsg.wardstudy.domain.attach.dto.AttachDTO; 4 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.web.multipart.MultipartFile; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.Size; 12 | import java.util.List; 13 | 14 | @Data 15 | @NoArgsConstructor 16 | public class StudyGroupRequest { 17 | 18 | @NotBlank 19 | @Size(min = 4, max = 99, message = "제목은 4글자 이상, 100글자 미만이어야 합니다.") 20 | private String title; 21 | 22 | @NotBlank 23 | @Size(min = 4, max = 1999, message = "내용은 4글자 이상, 2000글자 미만이어야 합니다.") 24 | private String content; 25 | private MultipartFile file; 26 | 27 | private List attachDTOS; 28 | 29 | @Builder 30 | public StudyGroupRequest(String title, String content) { 31 | this.title = title; 32 | this.content = content; 33 | } 34 | 35 | @Builder 36 | public StudyGroupRequest(String title, String content, MultipartFile file) { 37 | this.title = title; 38 | this.content = content; 39 | this.file = file; 40 | } 41 | 42 | public static StudyGroup mapToEntity(StudyGroupRequest studyGroupRequest) { 43 | return StudyGroup.builder() 44 | .title(studyGroupRequest.getTitle()) 45 | .content(studyGroupRequest.getContent()) 46 | .build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/dto/StudyGroupResponse.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.dto; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 4 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | public class StudyGroupResponse { 12 | 13 | private Long studyGroupId; 14 | private Long registerId; 15 | private String title; 16 | private String content; 17 | 18 | @Builder 19 | public StudyGroupResponse(Long studyGroupId, Long registerId, String title, String content) { 20 | this.studyGroupId = studyGroupId; 21 | this.registerId = registerId; 22 | this.title = title; 23 | this.content = content; 24 | } 25 | 26 | public static StudyGroupResponse mapToDto(StudyGroup savedGroup) { 27 | return StudyGroupResponse.builder() 28 | .studyGroupId(savedGroup.getId()) 29 | .title(savedGroup.getTitle()) 30 | .content(savedGroup.getContent()) 31 | .build(); 32 | } 33 | 34 | public static StudyGroupResponse mapToDto(UserGroup savedUserGroup) { 35 | return StudyGroupResponse.builder() 36 | .studyGroupId(savedUserGroup.getStudyGroup().getId()) 37 | .registerId(savedUserGroup.getUser().getId()) 38 | .title(savedUserGroup.getStudyGroup().getTitle()) 39 | .content(savedUserGroup.getStudyGroup().getContent()) 40 | .build(); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/reservation/ReservationQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.reservation; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import com.querydsl.jpa.impl.JPAQueryFactory; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import javax.persistence.EntityManager; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | import static com.dsg.wardstudy.domain.reservation.entity.QReservation.reservation; 13 | 14 | 15 | @Repository 16 | public class ReservationQueryRepository { 17 | 18 | private final JPAQueryFactory queryFactory; 19 | 20 | public ReservationQueryRepository(EntityManager em) { 21 | this.queryFactory = new JPAQueryFactory(em); 22 | } 23 | 24 | public List findByStartTimeAfterNowAndIsSentFalse(List sgIds) { 25 | return queryFactory 26 | .selectFrom(reservation) 27 | .where(reservation.startTime.after(LocalDateTime.now()) 28 | .and(reservation.studyGroup.id.in(sgIds)) 29 | .and(reservation.isEmailSent.eq(false)) 30 | ) 31 | .fetch(); 32 | } 33 | 34 | public Optional findByUserIdAndStudyGroupId(Long userId, Long studyGroupId) { 35 | return queryFactory 36 | .selectFrom(reservation) 37 | .where(reservation.user.id.eq(userId) 38 | .and(reservation.studyGroup.id.eq(studyGroupId)) 39 | ).stream().findFirst(); 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/attach/entity/Attach.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.attach.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.attach.dto.AttachDTO; 5 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | import lombok.*; 8 | 9 | import javax.persistence.*; 10 | 11 | @Entity 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @ToString(exclude = "studyGroup") 15 | @Table(name = "attach") 16 | public class Attach extends BaseTimeEntity { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | 22 | private String uuid; 23 | private String uploadPath; 24 | private String fileName; 25 | private boolean image; 26 | 27 | @JsonIgnore 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @JoinColumn(name = "study_group_id") 30 | private StudyGroup studyGroup; 31 | 32 | @Builder 33 | public Attach(Long id, String uuid, String uploadPath, String fileName, boolean image, StudyGroup studyGroup) { 34 | this.id = id; 35 | this.uuid = uuid; 36 | this.uploadPath = uploadPath; 37 | this.fileName = fileName; 38 | this.image = image; 39 | this.studyGroup = studyGroup; 40 | } 41 | 42 | public static Attach of(AttachDTO attachDTO, StudyGroup studyGroup) { 43 | return Attach.builder() 44 | .uuid(attachDTO.getUuid()) 45 | .uploadPath(attachDTO.getUploadPath()) 46 | .fileName(attachDTO.getFileName()) 47 | .image(attachDTO.isImage()) 48 | .studyGroup(studyGroup) 49 | .build(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | C:\\test_logs 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ${LOGS_PATH}/log4j2_ward-study.log 18 | ${LOGS_PATH}/log4j2.%d{yyyy-MM-dd}.%i.log.gz 19 | 20 | %d{yyyy-MM-dd HH:mm:ss} %5p [%c] %m%n 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/entity/Comment.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.comment.dto.CommentDto; 5 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 6 | import com.dsg.wardstudy.domain.user.entity.User; 7 | import lombok.*; 8 | 9 | import javax.persistence.*; 10 | 11 | @Getter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @Entity 14 | @Table(name = "comments") 15 | public class Comment extends BaseTimeEntity { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "comment_id", nullable = false) 20 | private Long id; 21 | 22 | private String name; 23 | 24 | private String email; 25 | 26 | private String body; 27 | 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | @JoinColumn(name = "study_group_id", nullable = false) 30 | @Setter 31 | private StudyGroup studyGroup; 32 | 33 | @ManyToOne(fetch = FetchType.LAZY) 34 | @JoinColumn(name = "user_id") 35 | @Setter 36 | private User user; 37 | 38 | @Builder 39 | public Comment(Long id, String name, String email, String body, StudyGroup studyGroup, User user) { 40 | this.id = id; 41 | this.name = name; 42 | this.email = email; 43 | this.body = body; 44 | this.studyGroup = studyGroup; 45 | this.user = user; 46 | } 47 | 48 | public static Comment of(CommentDto commentDto) { 49 | return Comment.builder() 50 | .name(commentDto.getName()) 51 | .email(commentDto.getEmail()) 52 | .body(commentDto.getBody()) 53 | .build(); 54 | } 55 | 56 | public void updateComment(CommentDto commentDto) { 57 | this.body = commentDto.getBody(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.utils; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import org.springframework.web.multipart.MultipartFile; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.util.zip.Deflater; 8 | import java.util.zip.Inflater; 9 | 10 | public class FileUtils { 11 | 12 | public static String getFileCodeName(MultipartFile file) { 13 | return RandomStringUtils.randomAlphanumeric(8) + "-" + file.getOriginalFilename(); 14 | } 15 | 16 | public static byte[] compressImage(byte[] data) { 17 | Deflater deflater = new Deflater(); 18 | deflater.setLevel(Deflater.BEST_COMPRESSION); 19 | deflater.setInput(data); 20 | deflater.finish(); 21 | 22 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length); 23 | byte[] tmp = new byte[4*1024]; 24 | while (!deflater.finished()) { 25 | int size = deflater.deflate(tmp); 26 | outputStream.write(tmp, 0, size); 27 | } 28 | try { 29 | outputStream.close(); 30 | } catch (Exception ignored) { 31 | } 32 | return outputStream.toByteArray(); 33 | } 34 | 35 | 36 | 37 | public static byte[] decompressImage(byte[] data) { 38 | Inflater inflater = new Inflater(); 39 | inflater.setInput(data); 40 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length); 41 | byte[] tmp = new byte[4*1024]; 42 | try { 43 | while (!inflater.finished()) { 44 | int count = inflater.inflate(tmp); 45 | outputStream.write(tmp, 0, count); 46 | } 47 | outputStream.close(); 48 | } catch (Exception ignored) { 49 | } 50 | return outputStream.toByteArray(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/entity/StudyGroup.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.attach.entity.Attach; 5 | import com.dsg.wardstudy.domain.comment.entity.Comment; 6 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import lombok.*; 9 | 10 | import javax.persistence.*; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Getter 15 | @Entity 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Table(name = "study_group") 18 | @ToString(of = {"id", "title", "content"}) 19 | public class StudyGroup extends BaseTimeEntity { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "study_group_id") 24 | private Long id; 25 | 26 | @Column(nullable = false) 27 | private String title; 28 | 29 | @Column(nullable = false) 30 | private String content; 31 | 32 | @OneToMany(mappedBy = "studyGroup", cascade = CascadeType.ALL) 33 | @JsonIgnore 34 | private List userGroups = new ArrayList<>(); 35 | 36 | @OneToMany(mappedBy = "studyGroup", cascade = CascadeType.ALL) 37 | private List attaches = new ArrayList<>(); 38 | 39 | @OneToMany(mappedBy = "studyGroup", cascade = CascadeType.ALL) 40 | private List comments = new ArrayList<>(); 41 | 42 | @OneToMany(mappedBy = "studyGroup", cascade = CascadeType.ALL) 43 | private List likes = new ArrayList<>(); 44 | 45 | @Builder 46 | public StudyGroup(Long id, String title, String content) { 47 | this.id = id; 48 | this.title = title; 49 | this.content = content; 50 | } 51 | 52 | public void update(String title, String content) { 53 | this.title = title; 54 | this.content = content; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | database: 5 | container_name: mysql_db 6 | image: mysql 7 | restart: unless-stopped 8 | env_file: 9 | - .env 10 | ports: 11 | - "3307:3306" 12 | volumes: 13 | - ./mysql/conf.d:/etc/mysql/conf.d # MySQL 설정 파일 위치 14 | command: 15 | - "mysqld" 16 | - "--character-set-server=utf8mb4" 17 | - "--collation-server=utf8mb4_unicode_ci" 18 | networks: 19 | - test_network 20 | 21 | redis-cache: 22 | image: redis:latest 23 | container_name: redis-cache 24 | command: redis-server --port 6379 25 | ports: 26 | - "6379:6379" 27 | networks: 28 | - test_network 29 | redis-session: 30 | image: redis:latest 31 | container_name: redis-session 32 | command: redis-server --port 6380 33 | ports: 34 | - "6380:6380" 35 | networks: 36 | - test_network 37 | 38 | application1: 39 | container_name: ward-study1 40 | restart: on-failure 41 | build: 42 | context: ./ 43 | dockerfile: Dockerfile 44 | ports: 45 | - "8081:8081" 46 | env_file: 47 | - .env 48 | depends_on: 49 | - database 50 | - redis-cache 51 | - redis-session 52 | networks: 53 | - test_network 54 | 55 | application2: 56 | container_name: ward-study2 57 | restart: on-failure 58 | build: 59 | context: ./ 60 | dockerfile: Dockerfile 61 | ports: 62 | - "8082:8081" 63 | env_file: 64 | - .env 65 | depends_on: 66 | - database 67 | - redis-cache 68 | - redis-session 69 | networks: 70 | - test_network 71 | 72 | application3: 73 | container_name: ward-study3 74 | restart: on-failure 75 | build: 76 | context: ./ 77 | dockerfile: Dockerfile 78 | ports: 79 | - "8083:8081" 80 | env_file: 81 | - .env 82 | depends_on: 83 | - database 84 | - redis-cache 85 | - redis-session 86 | networks: 87 | - test_network 88 | 89 | networks: 90 | test_network: 91 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | 2 | server: 3 | port: 8081 4 | 5 | spring: 6 | 7 | mvc: 8 | pathmatch: 9 | matching-strategy: ant_path_matcher # swagger 표시 위함 10 | 11 | batch: 12 | job: 13 | enabled: false 14 | names: ${job.name:NONE} 15 | jdbc: 16 | initialize-schema: never 17 | # main: 18 | # web-application-type: none 19 | 20 | jackson: 21 | serialization: 22 | fail-on-empty-beans: false 23 | 24 | session: 25 | store-type: redis 26 | 27 | redis: 28 | cache: 29 | host: redis-cache 30 | port: 6379 31 | session: 32 | host: redis-session 33 | port: 6380 34 | 35 | datasource: 36 | driver-class-name: com.mysql.cj.jdbc.Driver 37 | url: jdbc:mysql://mysql_db:3306/${MYSQL_DATABASE}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 38 | username: ${SPRING_DATASOURCE_USERNAME} 39 | password: ${SPRING_DATASOURCE_PASSWORD} 40 | 41 | jpa: 42 | open-in-view: true 43 | hibernate: 44 | ddl-auto: update # prod 배포시 validate 45 | naming: 46 | physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 47 | use-new-id-generator-mappings: false 48 | show-sql: true 49 | properties: 50 | hibernate.format_sql: true 51 | dialect: org.hibernate.dialect.MySQL8Dialect 52 | 53 | mail: 54 | host: smtp.gmail.com 55 | port: 587 56 | username: ${SPRING_MAIL_ID} 57 | password: ${SPRING_MAIL_PASSWORD} 58 | properties: 59 | mail: 60 | smtp: 61 | starttls: 62 | enable: true 63 | 64 | logging: 65 | level: 66 | org.hibernate.SQL: debug 67 | 68 | app: 69 | firebase-configuration-file: ./file-storage-68596-firebase-adminsdk-5bv5f-260ab6cc65.json 70 | firebase-bucket: file-storage-68596.appspot.com 71 | 72 | 73 | naver: 74 | url: 75 | search: 76 | local: https://openapi.naver.com/v1/search/local.json 77 | image: https://openapi.naver.com/v1/search/image 78 | client: 79 | id: Zi3o1uQftp59zuIqEAz4 80 | secret: iy6YKSWpLM 81 | -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | 2 | server: 3 | port: 8083 4 | 5 | spring: 6 | 7 | mvc: 8 | pathmatch: 9 | matching-strategy: ant_path_matcher # swagger 표시 위함 10 | 11 | batch: 12 | job: 13 | enabled: false 14 | names: ${job.name:NONE} 15 | jdbc: 16 | initialize-schema: never 17 | # main: 18 | # web-application-type: none 19 | 20 | jackson: 21 | serialization: 22 | fail-on-empty-beans: false 23 | 24 | session: 25 | store-type: redis 26 | 27 | redis: 28 | cache: 29 | host: localhost 30 | port: 6379 31 | session: 32 | host: localhost 33 | port: 6380 34 | 35 | datasource: 36 | driver-class-name: com.mysql.cj.jdbc.Driver 37 | url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 38 | username: ${SPRING_DATASOURCE_USERNAME} 39 | password: ${SPRING_DATASOURCE_PASSWORD} 40 | 41 | jpa: 42 | open-in-view: true 43 | hibernate: 44 | ddl-auto: update # prod 배포시 validate 45 | naming: 46 | physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl 47 | use-new-id-generator-mappings: false 48 | show-sql: true 49 | properties: 50 | hibernate.format_sql: true 51 | dialect: org.hibernate.dialect.MySQL8Dialect 52 | 53 | mail: 54 | host: smtp.gmail.com 55 | port: 587 56 | username: ${SPRING_MAIL_ID} 57 | password: ${SPRING_MAIL_PASSWORD} 58 | properties: 59 | mail: 60 | smtp: 61 | starttls: 62 | enable: true 63 | 64 | logging: 65 | level: 66 | org.hibernate.SQL: debug 67 | 68 | app: 69 | firebase-configuration-file: ./file-storage-68596-firebase-adminsdk-5bv5f-260ab6cc65.json 70 | firebase-bucket: file-storage-68596.appspot.com 71 | 72 | 73 | naver: 74 | url: 75 | search: 76 | local: https://openapi.naver.com/v1/search/local.json 77 | image: https://openapi.naver.com/v1/search/image 78 | client: 79 | id: 6PagL7E4PzGch6__yf0F 80 | secret: DGgEjvKNfh 81 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/dto/WishDto.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.dto; 2 | 3 | import com.dsg.wardstudy.domain.wish.entity.Wish; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | public class WishDto { 13 | 14 | private Long wishId; 15 | private String title; // 스터디카페명 16 | private String category; // 카테고리 17 | private String address; // 주소 18 | private String roadAddress; // 도로명 19 | private String homePageLink; // 홈페이지 주소 20 | private String imageLink; // 이미지 주소 21 | private boolean isVisit; // 방문여부 22 | private int visitCount; // 방문 카운트 23 | private LocalDateTime lastVisitDate; // 마지막 방문일자 24 | 25 | @Builder 26 | public WishDto(Long wishId, String title, String category, String address, String roadAddress, String homePageLink, String imageLink, boolean isVisit, int visitCount, LocalDateTime lastVisitDate) { 27 | this.wishId = wishId; 28 | this.title = title; 29 | this.category = category; 30 | this.address = address; 31 | this.roadAddress = roadAddress; 32 | this.homePageLink = homePageLink; 33 | this.imageLink = imageLink; 34 | this.isVisit = isVisit; 35 | this.visitCount = visitCount; 36 | this.lastVisitDate = lastVisitDate; 37 | } 38 | 39 | public static WishDto mapToDto(Wish wish) { 40 | return WishDto.builder() 41 | .wishId(wish.getId()) 42 | .title(wish.getTitle()) 43 | .category(wish.getCategory()) 44 | .address(wish.getAddress()) 45 | .roadAddress(wish.getRoadAddress()) 46 | .homePageLink(wish.getHomePageLink()) 47 | .imageLink(wish.getImageLink()) 48 | .isVisit(wish.isVisit()) 49 | .visitCount(wish.getVisitCount()) 50 | .lastVisitDate(wish.getLastVisitDate()) 51 | .build(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/redis/RedisSessionConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.redis; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.connection.RedisConnectionFactory; 7 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 8 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 11 | import org.springframework.data.redis.serializer.StringRedisSerializer; 12 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 13 | 14 | @Configuration 15 | @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60) /* 세션 만료 시간 : 60초 */ 16 | public class RedisSessionConfig { 17 | 18 | @Value("${spring.redis.session.host}") 19 | private String redisSessionHost; 20 | 21 | @Value("${spring.redis.session.port}") 22 | private int redisSessionPort; 23 | 24 | 25 | // Creating Connection with Redis session 26 | @Bean({"redisConnectionFactory", "redisSessionConnectionFactory"}) 27 | public RedisConnectionFactory redisConnectionFactory() { 28 | RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); 29 | redisStandaloneConfiguration.setHostName(redisSessionHost); 30 | redisStandaloneConfiguration.setPort(redisSessionPort); 31 | 32 | return new LettuceConnectionFactory(redisStandaloneConfiguration); 33 | } 34 | 35 | @Bean(name = "redisTemplate") 36 | public RedisTemplate redisTemplate() { 37 | RedisTemplate redisTemplate = new RedisTemplate<>(); 38 | redisTemplate.setConnectionFactory(redisConnectionFactory()); 39 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 40 | redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); 41 | return redisTemplate; 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/adapter/mail/MailMessageGenerator.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.adapter.mail; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.time.LocalDateTime; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | @Component 12 | public class MailMessageGenerator { 13 | 14 | public String toMessage(String userName, List reservations) { 15 | return String.format( 16 | "%s 님, ward-study 예약룸 알림 전달드립니다.\n", userName) 17 | + 18 | reservations.stream() 19 | .map(r -> String.format("

스터디그룹: %s, 스터디리더: %s 님

\n" + 20 | "

룸: %s, 예약시간: %s [%s]~[%s] 으로 등록되었습니다! 😣

\n", 21 | r.getStudyGroup().getTitle(), r.getUser().getName(), 22 | r.getRoom().getName(),formatterStrYearMonthDay(r.getStartTime()), 23 | formatterStrTime(r.getStartTime()), formatterStrTime(r.getEndTime()))) 24 | .collect(Collectors.joining()); 25 | } 26 | 27 | public String toKafkaMessage(String userName, Reservation reservation) { 28 | return String.format("%s님, ward-study 예약룸 알림 전달드립니다.\n", userName) + 29 | String.format("

스터디그룹: %s, 스터디리더: %s님

\n", reservation.getStudyGroup().getTitle(), reservation.getUser().getName()) + 30 | String.format("

룸: %s, 예약시간: %s [%s]~[%s] 으로 등록되었습니다! 😣

\n", 31 | reservation.getRoom().getName(), formatterStrYearMonthDay(reservation.getStartTime()), 32 | formatterStrTime(reservation.getStartTime()), formatterStrTime(reservation.getEndTime())); 33 | } 34 | 35 | private String formatterStrYearMonthDay(LocalDateTime time) { 36 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 37 | return time.format(formatter); 38 | } 39 | 40 | private String formatterStrTime(LocalDateTime time) { 41 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); 42 | return time.format(formatter); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud-analyze.yml: -------------------------------------------------------------------------------- 1 | name: F-Lab SonarCloud Code Analyze 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | workflow_dispatch: 7 | 8 | env: 9 | CACHED_DEPENDENCIES_PATHS: '**/node_modules' 10 | 11 | jobs: 12 | CodeAnalyze: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set SonarCloud Project Key 21 | run: | 22 | REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2) 23 | ORG_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1) 24 | SONAR_PROJECT_KEY="${ORG_NAME}_${REPO_NAME}" 25 | echo "SONAR_PROJECT_KEY=$SONAR_PROJECT_KEY" >> $GITHUB_ENV 26 | 27 | - name: Set up JDK 28 | uses: actions/setup-java@v2 29 | with: 30 | java-version: '19' 31 | distribution: 'adopt' 32 | 33 | - name: Create Sonar Gradle File 34 | run: | 35 | insert_string="plugins { id 'org.sonarqube' version '4.4.1.3373' }" 36 | if [ -f "build.gradle" ]; then 37 | echo "$insert_string" > temp.gradle 38 | cat build.gradle >> temp.gradle 39 | echo "" >> temp.gradle 40 | echo "sonarqube {" >> temp.gradle 41 | echo " properties {" >> temp.gradle 42 | echo " property 'sonar.java.binaries', '**'" >> temp.gradle 43 | echo " }" >> temp.gradle 44 | echo "}" >> temp.gradle 45 | mv temp.gradle build.gradle 46 | else 47 | echo "$insert_string" > build.gradle 48 | echo "" >> build.gradle 49 | echo "sonarqube {" >> build.gradle 50 | echo " properties {" >> build.gradle 51 | echo " property 'sonar.java.binaries', '**'" >> build.gradle 52 | echo " }" >> build.gradle 53 | echo "}" >> build.gradle 54 | fi 55 | 56 | 57 | - name: Analyze 58 | run: ./gradlew sonar -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.organization=f-lab-edu-1 -Dsonar.host.url=https://sonarcloud.io -Dsonar.token=${{ secrets.SECRET_SONARQUBE }} -Dsonar.gradle.skipCompile=true 59 | env: 60 | SONAR_TOKEN: ${{ secrets.SECRET_SONARQUBE }} 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/exception/WSApiException.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class WSApiException extends RuntimeException { 7 | 8 | private String resourceName; 9 | private String fieldName; 10 | private Long fieldValue; 11 | private String fieldValueStr; 12 | private final ErrorCode errorCode; 13 | 14 | public WSApiException(ErrorCode errorCode) { 15 | super(errorCode.getErrorMsg()); 16 | this.errorCode = errorCode; 17 | } 18 | 19 | public WSApiException(ErrorCode errorCode, String message) { 20 | super(message); 21 | this.errorCode = errorCode; 22 | } 23 | 24 | // 그외 25 | public WSApiException(ErrorCode errorCode, String resourceName, String fieldName, Long fieldValue) { 26 | super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); // 'StudyGroup' not found with 'id' : '1' 27 | this.resourceName = resourceName; 28 | this.fieldName = fieldName; 29 | this.fieldValue = fieldValue; 30 | this.errorCode = errorCode; 31 | } 32 | 33 | public WSApiException(ErrorCode errorCode, String resourceName, Long fieldValue) { 34 | super(String.format("%s is duplicated by '%s'", resourceName, fieldValue)); // 'StudyGroup' is duplicated by '1' 35 | this.resourceName = resourceName; 36 | this.fieldValue = fieldValue; 37 | this.errorCode = errorCode; 38 | } 39 | // Reservation 40 | public WSApiException(ErrorCode errorCode, String resourceName, String fieldName, String fieldValueStr) { 41 | super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValueStr)); // 'Reservation' not found with 'id' : '1' 42 | this.resourceName = resourceName; 43 | this.fieldName = fieldName; 44 | this.fieldValueStr = fieldValueStr; 45 | this.errorCode = errorCode; 46 | } 47 | 48 | public WSApiException(ErrorCode errorCode, String resourceName, String fieldValueStr) { 49 | super(String.format("%s is duplicated by '%s'", resourceName, fieldValueStr)); // 'Reservation' is duplicated by '1' 50 | this.resourceName = resourceName; 51 | this.fieldValueStr = fieldValueStr; 52 | this.errorCode = errorCode; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/dto/ReservationDetails.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.dto; 2 | 3 | 4 | import com.dsg.wardstudy.common.utils.TimeParsingUtils; 5 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 6 | import com.fasterxml.jackson.annotation.JsonFormat; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Data 12 | @NoArgsConstructor 13 | public class ReservationDetails { 14 | 15 | private String reservationToken; 16 | 17 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 18 | private String startTime; 19 | 20 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 21 | private String endTime; 22 | 23 | 24 | private Long registerId; 25 | 26 | private String registerEmail; 27 | 28 | private Long studyGroupId; 29 | 30 | private String studyGroupTitle; 31 | 32 | private Long roomId; 33 | private String roomName; 34 | 35 | @Builder 36 | public ReservationDetails(String reservationToken, String startTime, String endTime, Long registerId, String registerEmail, Long studyGroupId, String studyGroupTitle, Long roomId, String roomName) { 37 | this.reservationToken = reservationToken; 38 | this.startTime = startTime; 39 | this.endTime = endTime; 40 | this.registerId = registerId; 41 | this.registerEmail = registerEmail; 42 | this.studyGroupId = studyGroupId; 43 | this.studyGroupTitle = studyGroupTitle; 44 | this.roomId = roomId; 45 | this.roomName = roomName; 46 | } 47 | 48 | public static ReservationDetails mapToDto(Reservation reservation) { 49 | return ReservationDetails.builder() 50 | .reservationToken(reservation.getReservationToken()) 51 | .startTime(TimeParsingUtils.formatterString(reservation.getStartTime())) 52 | .endTime(TimeParsingUtils.formatterString(reservation.getEndTime())) 53 | .registerId(reservation.getUser().getId()) 54 | .registerEmail(reservation.getUser().getEmail()) 55 | .studyGroupId(reservation.getStudyGroup().getId()) 56 | .studyGroupTitle(reservation.getStudyGroup().getTitle()) 57 | .roomId(reservation.getRoom().getId()) 58 | .roomName(reservation.getRoom().getName()) 59 | .build(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/repository/reservation/ReservationRepository.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.reservation; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.Lock; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | 11 | import javax.persistence.LockModeType; 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | public interface ReservationRepository extends JpaRepository { 17 | 18 | 19 | @Lock(LockModeType.PESSIMISTIC_WRITE) 20 | @Query("select r from Reservation r where r.reservationToken = :reservationToken") 21 | Optional findByTokenLock(@Param("reservationToken") String reservationToken); 22 | 23 | Page findBy(Pageable pageable); 24 | 25 | @Query("select r from Reservation r left join fetch r.room where r.room.id = :roomId") 26 | List findByRoomId(@Param("roomId") Long roomId); 27 | 28 | @Query("select r from Reservation r left join fetch r.room where r.room.id = :roomId " + 29 | "and r.startTime >= :sTime and r.endTime <= :eTime") 30 | List findByRoomIdAndTimePeriod(@Param("roomId") Long roomId, 31 | @Param("sTime") LocalDateTime sTime, 32 | @Param("eTime") LocalDateTime eTime); 33 | 34 | @Query("select r from Reservation r left join fetch r.room where r.room.id = :roomId " + 35 | "or r.startTime = :sTime or r.endTime = :eTime") 36 | List findByRoomIdAndTime(@Param("roomId") Long roomId, 37 | @Param("sTime") LocalDateTime sTime, 38 | @Param("eTime") LocalDateTime eTime); 39 | 40 | 41 | @Query("select r from Reservation r left join fetch r.room where r.room.id = :roomId " + 42 | "and r.reservationToken = :reservationToken") 43 | Optional findByRoomIdAndToken(@Param("roomId") Long roomId, @Param("reservationToken") String reservationToken); 44 | 45 | @Query("select r from Reservation r left join fetch r.studyGroup where r.studyGroup.id in :sgIds") 46 | List findByStudyGroupIds(@Param("sgIds") List sgIds); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.controller; 2 | 3 | import com.dsg.wardstudy.common.auth.AuthUser; 4 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 5 | import com.dsg.wardstudy.domain.user.dto.LoginDto; 6 | import com.dsg.wardstudy.domain.user.dto.SignUpRequest; 7 | import com.dsg.wardstudy.domain.user.dto.SignUpResponse; 8 | import com.dsg.wardstudy.domain.user.dto.UserInfo; 9 | import com.dsg.wardstudy.domain.user.service.LoginService; 10 | import com.dsg.wardstudy.domain.user.service.UserService; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.log4j.Log4j2; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.validation.Valid; 18 | 19 | @Log4j2 20 | @RestController 21 | @RequestMapping("/users") 22 | @RequiredArgsConstructor 23 | public class UserController { 24 | private final UserService userService; 25 | private final LoginService loginService; 26 | 27 | /** 28 | * 회원가입(register) 29 | */ 30 | @PostMapping("/signup") 31 | public ResponseEntity signup( 32 | @Valid @RequestBody SignUpRequest signUpDto 33 | ) { 34 | log.info("users signup, signUpDto: {}", signUpDto); 35 | SignUpResponse signUpResponse = loginService.signUp(signUpDto); 36 | 37 | return new ResponseEntity<>(signUpResponse, HttpStatus.CREATED); 38 | } 39 | 40 | @PostMapping("/login") 41 | public ResponseEntity login( 42 | @RequestBody LoginDto loginDto 43 | ) { 44 | loginService.loginUser(loginDto); 45 | return ResponseEntity.ok("login success!"); 46 | } 47 | 48 | @PostMapping("/logout") 49 | public ResponseEntity login() { 50 | loginService.logoutUser(); 51 | return ResponseEntity.ok("logout success!"); 52 | } 53 | 54 | /** 55 | * 사용자 회원정보 조회 56 | * 57 | * @return 58 | */ 59 | @GetMapping("/{userId}") 60 | public ResponseEntity getUser(@PathVariable("userId") Long userId) { 61 | UserInfo userInfo = userService.getUser(userId); 62 | log.info("userInfo: {}", userInfo); 63 | 64 | return new ResponseEntity<>(userInfo, HttpStatus.OK); 65 | } 66 | 67 | /** 68 | * 사용자 서비스 탈퇴 69 | * 70 | * @param userId 71 | * @return 72 | */ 73 | @DeleteMapping 74 | public ResponseEntity userWithdraw(@AuthUser Long userId) { 75 | userService.withdrawUser(userId); 76 | return ResponseEntity.ok("userWithdraw success"); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/dto/ReservationCommand.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.dto; 2 | 3 | import com.dsg.wardstudy.common.utils.TimeParsingUtils; 4 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 5 | import com.dsg.wardstudy.domain.reservation.entity.Room; 6 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 7 | import com.dsg.wardstudy.domain.user.entity.User; 8 | import lombok.*; 9 | 10 | public class ReservationCommand { 11 | 12 | @Getter 13 | @Setter 14 | @ToString 15 | @NoArgsConstructor 16 | public static class RegisterReservation { 17 | 18 | private Long userId; 19 | private String startTime; 20 | private String endTime; 21 | 22 | 23 | @Builder 24 | public RegisterReservation(Long userId, String startTime, String endTime) { 25 | this.userId = userId; 26 | this.startTime = startTime; 27 | this.endTime = endTime; 28 | } 29 | 30 | public Reservation mapToEntity(User user, StudyGroup studyGroup, Room room) { 31 | 32 | return Reservation.builder() 33 | .startTime(TimeParsingUtils.formatterLocalDateTime(startTime)) 34 | .endTime(TimeParsingUtils.formatterLocalDateTime(endTime)) 35 | .user(user) 36 | .studyGroup(studyGroup) 37 | .room(room) 38 | .build(); 39 | 40 | } 41 | 42 | } 43 | 44 | @Getter 45 | @Setter 46 | @ToString 47 | @NoArgsConstructor 48 | public static class UpdateReservation { 49 | 50 | private Long userId; 51 | 52 | private Long studyGroupId; 53 | 54 | private String startTime; 55 | 56 | private String endTime; 57 | 58 | 59 | @Builder 60 | public UpdateReservation(Long userId, Long studyGroupId, String startTime, String endTime) { 61 | this.userId = userId; 62 | this.studyGroupId = studyGroupId; 63 | this.startTime = startTime; 64 | this.endTime = endTime; 65 | } 66 | 67 | public Reservation mapToEntity(ValidateFindByIdDto validateFindByIdDto) { 68 | 69 | return Reservation.builder() 70 | .startTime(TimeParsingUtils.formatterLocalDateTime(startTime)) 71 | .endTime(TimeParsingUtils.formatterLocalDateTime(endTime)) 72 | .user(validateFindByIdDto.getUser()) 73 | .studyGroup(validateFindByIdDto.getStudyGroup()) 74 | .room(validateFindByIdDto.getRoom()) 75 | .build(); 76 | 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.service; 2 | 3 | import com.dsg.wardstudy.common.exception.ErrorCode; 4 | import com.dsg.wardstudy.common.exception.WSApiException; 5 | import com.dsg.wardstudy.common.utils.Encryptor; 6 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 7 | import com.dsg.wardstudy.domain.user.entity.User; 8 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 9 | import com.dsg.wardstudy.domain.user.dto.*; 10 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 11 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 12 | import com.dsg.wardstudy.repository.user.UserRepository; 13 | import com.dsg.wardstudy.domain.user.constant.UserType; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.log4j.Log4j2; 16 | import org.springframework.stereotype.Service; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | @Log4j2 20 | @Service 21 | @RequiredArgsConstructor 22 | public class UserServiceImpl implements UserService { 23 | 24 | private final UserRepository userRepository; 25 | 26 | 27 | @Override 28 | @Transactional 29 | public UserInfo create(SignUpRequest signUpRequest) { 30 | 31 | // pw 암호화해서 저장 32 | User savedUser = userRepository.save(User.of(signUpRequest)); 33 | log.info("create savedUser: {}", savedUser); 34 | return UserInfo.mapToDto(savedUser); 35 | } 36 | 37 | @Override 38 | @Transactional(readOnly = true) 39 | public UserInfo getUser(Long userId) { 40 | 41 | User findUser = userRepository.findById(userId) 42 | .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER)); 43 | log.info("getById findUser: {}", findUser); 44 | return UserInfo.mapToDto(findUser); 45 | } 46 | 47 | 48 | @Override 49 | @Transactional(readOnly = true) 50 | public LoginDto getByEmailAndPassword(String email, String password) { 51 | // 파라미터 password가 origin 52 | User user = userRepository.findByEmail(email) 53 | .map(u -> Encryptor.isMatch(password, u.getPassword()) ? u : null) 54 | .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER)); 55 | log.info("getByEmailAndPassword user : {}", user); 56 | 57 | return LoginDto.mapToDto(user); 58 | } 59 | 60 | @Override 61 | @Transactional 62 | public void withdrawUser(Long userId) { 63 | User findUser = userRepository.findById(userId) 64 | .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER)); 65 | log.info("withdrawUser, findUser: {}", findUser); 66 | findUser.withdrawUser(true); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/entity/Wish.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.entity; 2 | 3 | import com.dsg.wardstudy.domain.wish.dto.WishDto; 4 | import lombok.AccessLevel; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import javax.persistence.*; 10 | import java.time.LocalDateTime; 11 | 12 | @Getter 13 | @Entity 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @Table(name = "wish") 16 | public class Wish { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | @Column(name = "wish_id") 21 | private Long id; 22 | 23 | private String title; // 음식명, 장소명 24 | private String category; // 카테고리 25 | private String address; // 주소 26 | 27 | @Column(name = "road_address") 28 | private String roadAddress; // 도로명 29 | 30 | @Column(name = "home_page_link") 31 | private String homePageLink; // 홈페이지 주소 32 | 33 | @Column(name = "image_link") 34 | private String imageLink; // 음식, 가게 이미지 주소 35 | 36 | @Column(name = "is_visit") 37 | private boolean isVisit; // 방문여부 38 | 39 | @Column(name = "visit_count") 40 | private int visitCount; // 방문 카운트 41 | 42 | @Column(name = "last_visit_date") 43 | private LocalDateTime lastVisitDate; // 마지막 방문일자 44 | 45 | 46 | @Builder 47 | public Wish(Long id, String title, String category, String address, String roadAddress, String homePageLink, String imageLink, boolean isVisit, int visitCount, LocalDateTime lastVisitDate) { 48 | this.id = id; 49 | this.title = title; 50 | this.category = category; 51 | this.address = address; 52 | this.roadAddress = roadAddress; 53 | this.homePageLink = homePageLink; 54 | this.imageLink = imageLink; 55 | this.isVisit = isVisit; 56 | this.visitCount = visitCount; 57 | this.lastVisitDate = lastVisitDate; 58 | } 59 | 60 | public static Wish of(WishDto wishDto) { 61 | return Wish.builder() 62 | .id(wishDto.getWishId()) 63 | .title(wishDto.getTitle()) 64 | .category(wishDto.getCategory()) 65 | .address(wishDto.getAddress()) 66 | .roadAddress(wishDto.getRoadAddress()) 67 | .homePageLink(wishDto.getHomePageLink()) 68 | .imageLink(wishDto.getImageLink()) 69 | .isVisit(wishDto.isVisit()) 70 | .visitCount(wishDto.getVisitCount()) 71 | .lastVisitDate(wishDto.getLastVisitDate()) 72 | .build(); 73 | } 74 | 75 | public void addVisit() { 76 | this.isVisit = true; 77 | this.visitCount = this.visitCount + 1; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/entity/Reservation.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.entity; 2 | 3 | import com.dsg.wardstudy.common.utils.TimeParsingUtils; 4 | import com.dsg.wardstudy.common.utils.TokenGenerator; 5 | import com.dsg.wardstudy.domain.BaseTimeEntity; 6 | import com.dsg.wardstudy.domain.reservation.dto.ReservationCommand; 7 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 8 | import com.dsg.wardstudy.domain.user.entity.User; 9 | import com.fasterxml.jackson.annotation.JsonFormat; 10 | import lombok.*; 11 | 12 | import javax.persistence.*; 13 | import java.time.LocalDateTime; 14 | 15 | @Getter 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Entity 18 | @ToString(of = {"reservationToken", "startTime", "endTime", "isEmailSent"}) 19 | @Table(name = "reservation") 20 | public class Reservation extends BaseTimeEntity { 21 | 22 | private static final String RESERVATION_PREFIX = "reserv_"; 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "reservation_id") 27 | private Long id; 28 | 29 | @Column(name = "reservation_token", unique = true) 30 | private String reservationToken; 31 | 32 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 33 | @Column(name = "start_time") 34 | private LocalDateTime startTime; 35 | 36 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 37 | @Column(name = "end_time") 38 | private LocalDateTime endTime; 39 | 40 | @ManyToOne(fetch = FetchType.LAZY) 41 | @JoinColumn(name = "register_id") 42 | private User user; 43 | 44 | @ManyToOne(fetch = FetchType.LAZY) 45 | @JoinColumn(name = "study_group_id") 46 | private StudyGroup studyGroup; 47 | 48 | @ManyToOne(fetch = FetchType.LAZY) 49 | @JoinColumn(name = "room_id") 50 | private Room room; 51 | 52 | @Column(name = "is_email_sent") 53 | private boolean isEmailSent; 54 | 55 | @Builder 56 | public Reservation(LocalDateTime startTime, LocalDateTime endTime, User user, StudyGroup studyGroup, Room room, boolean isEmailSent) { 57 | this.reservationToken = TokenGenerator.randomCharacterWithPrefix(RESERVATION_PREFIX); 58 | this.startTime = startTime; 59 | this.endTime = endTime; 60 | this.user = user; 61 | this.studyGroup = studyGroup; 62 | this.room = room; 63 | this.isEmailSent = isEmailSent; 64 | } 65 | 66 | public void changeIsEmailSent(boolean isEmailSent) { 67 | this.isEmailSent = isEmailSent; 68 | } 69 | 70 | public void update(ReservationCommand.UpdateReservation updateReservation) { 71 | this.startTime = TimeParsingUtils.formatterLocalDateTime(updateReservation.getStartTime()); 72 | this.endTime = TimeParsingUtils.formatterLocalDateTime(updateReservation.getEndTime()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/redis/RedisCacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.redis; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.redis.cache.RedisCacheConfiguration; 9 | import org.springframework.data.redis.cache.RedisCacheManager; 10 | import org.springframework.data.redis.connection.RedisConnectionFactory; 11 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 12 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 13 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 14 | import org.springframework.data.redis.serializer.RedisSerializationContext; 15 | import org.springframework.data.redis.serializer.StringRedisSerializer; 16 | 17 | import java.time.Duration; 18 | 19 | @Configuration 20 | @EnableCaching 21 | public class RedisCacheConfig { 22 | 23 | @Value("${spring.redis.cache.host}") 24 | private String redisCacheHost; 25 | 26 | @Value("${spring.redis.cache.port}") 27 | private int redisCachePort; 28 | 29 | 30 | // Creating Connection with Redis cache 31 | @Bean(name = "redisCacheConnectionFactory") 32 | public RedisConnectionFactory redisConnectionFactory() { 33 | 34 | RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); 35 | redisStandaloneConfiguration.setHostName(redisCacheHost); 36 | redisStandaloneConfiguration.setPort(redisCachePort); 37 | 38 | return new LettuceConnectionFactory(redisStandaloneConfiguration); 39 | } 40 | 41 | 42 | /** 43 | * Redis Cache 설정 44 | * cache expire : 하루 (StudyGroupList, ReservationList) 45 | */ 46 | @Bean 47 | public RedisCacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { 48 | 49 | RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration 50 | .defaultCacheConfig() 51 | .serializeKeysWith( 52 | RedisSerializationContext.SerializationPair 53 | .fromSerializer(new StringRedisSerializer())) 54 | .serializeValuesWith( 55 | RedisSerializationContext.SerializationPair 56 | .fromSerializer(new GenericJackson2JsonRedisSerializer())) 57 | .entryTtl(Duration.ofDays(1L)); 58 | 59 | return RedisCacheManager 60 | .RedisCacheManagerBuilder 61 | .fromConnectionFactory(redisConnectionFactory) 62 | .cacheDefaults(redisCacheConfiguration) 63 | .build(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/file/FileService.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.file; 2 | 3 | import com.dsg.wardstudy.common.exception.ErrorCode; 4 | import com.dsg.wardstudy.common.exception.WSApiException; 5 | import com.dsg.wardstudy.common.utils.FileUtils; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | 15 | 16 | @Log4j2 17 | @Service 18 | @RequiredArgsConstructor 19 | public class FileService { 20 | 21 | private final String FOLDER_PATH = "C:\\WebStudy\\f-lab\\ward-study-reservation\\Files-Upload\\"; 22 | private final FileRepository storageRepository; 23 | 24 | // DB에 저장 25 | public FileResponse upload(MultipartFile file) throws IOException { 26 | log.info("upload file: {}", file); 27 | FileData fileData = storageRepository.save( 28 | FileData.builder() 29 | .name(file.getOriginalFilename()) 30 | .type(file.getContentType()) 31 | .size(file.getSize()) 32 | .imageData(FileUtils.compressImage(file.getBytes())) 33 | .build()); 34 | 35 | return FileResponse.of(fileData); 36 | } 37 | 38 | public byte[] downloadImage(String fileName) { 39 | FileData fileData = storageRepository.findByName(fileName) 40 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY)); 41 | 42 | log.info("download fileData: {}", fileData); 43 | 44 | return FileUtils.decompressImage(fileData.getImageData()); 45 | } 46 | 47 | // 파일경로에 저장 48 | // 업로드 49 | public FileResponse uploadImageToFileSystem(MultipartFile file) throws IOException { 50 | String fileCodeName = FileUtils.getFileCodeName(file); 51 | log.info("upload fileCodeName: {}", fileCodeName); 52 | String filePath = FOLDER_PATH + fileCodeName; 53 | 54 | FileData fileData = storageRepository.save( 55 | FileData.builder() 56 | .name(fileCodeName) 57 | .type(file.getContentType()) 58 | .size(file.getSize()) 59 | .filePath(filePath) 60 | .build() 61 | ); 62 | file.transferTo(new File(filePath)); 63 | return FileResponse.of(fileData); 64 | } 65 | 66 | // 다운로드 67 | public byte[] downloadImageFromFileSystem(String fileCodeName) throws IOException { 68 | FileData fileData = storageRepository.findByName(fileCodeName) 69 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY)); 70 | 71 | String filePath = fileData.getFilePath(); 72 | log.info("download filePath: {}", filePath); 73 | return Files.readAllBytes(new File(filePath).toPath()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/common/exception/WSExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.common.exception; 2 | 3 | import com.dsg.wardstudy.common.utils.TimeParsingUtils; 4 | import lombok.extern.log4j.Log4j2; 5 | import org.springframework.context.support.DefaultMessageSourceResolvable; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.MethodArgumentNotValidException; 10 | import org.springframework.web.bind.annotation.ControllerAdvice; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.context.request.WebRequest; 13 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 14 | 15 | import java.time.LocalDateTime; 16 | import java.util.Optional; 17 | 18 | import static com.dsg.wardstudy.common.exception.ErrorHttpStatusMapper.mapToStatus; 19 | 20 | 21 | @Log4j2 22 | @ControllerAdvice 23 | public class WSExceptionHandler extends ResponseEntityExceptionHandler { 24 | 25 | // handle specific exceptions 26 | @ExceptionHandler(WSApiException.class) 27 | public ResponseEntity handleWSApiException( 28 | WSApiException exception, 29 | WebRequest request) { 30 | 31 | 32 | log.error("WSApiException: ", exception); 33 | 34 | ErrorDetails errorDetails = ErrorDetails.builder() 35 | .date(TimeParsingUtils.formatterString(LocalDateTime.now())) 36 | .message(exception.getMessage()) 37 | .description(request.getDescription(false)) 38 | .errorCode(exception.getErrorCode()) 39 | .build(); 40 | 41 | log.info("errorDetails: {}", errorDetails); 42 | return new ResponseEntity<>(errorDetails, mapToStatus(errorDetails.getErrorCode())); 43 | } 44 | 45 | //BindingResult Validation 처리 46 | @Override 47 | protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, 48 | HttpHeaders headers, 49 | HttpStatus status, 50 | WebRequest request) { 51 | log.error("MethodArgumentNotValidException: ", ex); 52 | 53 | ErrorDetails errorDetails = ErrorDetails.builder() 54 | .date(TimeParsingUtils.formatterString(LocalDateTime.now())) 55 | .message(Optional.ofNullable(ex.getBindingResult() 56 | .getFieldError()) 57 | .map(DefaultMessageSourceResolvable::getDefaultMessage) 58 | .orElse(ex.getMessage())) 59 | .description(request.getDescription(false)) 60 | .errorCode(ErrorCode.INVALID_REQUEST) 61 | .build(); 62 | 63 | log.error("errorDetails: {}", errorDetails); 64 | return new ResponseEntity<>(errorDetails, mapToStatus(errorDetails.getErrorCode())); 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.entity; 2 | 3 | import com.dsg.wardstudy.domain.BaseTimeEntity; 4 | import com.dsg.wardstudy.domain.comment.entity.Comment; 5 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 6 | import com.dsg.wardstudy.domain.studyGroup.entity.Like; 7 | import com.dsg.wardstudy.domain.user.dto.LoginDto; 8 | import com.dsg.wardstudy.domain.user.dto.SignUpRequest; 9 | import com.fasterxml.jackson.annotation.JsonIgnore; 10 | import lombok.*; 11 | 12 | import javax.persistence.*; 13 | import java.time.LocalDateTime; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | import static com.dsg.wardstudy.common.utils.Encryptor.*; 18 | 19 | @Getter 20 | @Entity 21 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 22 | @Table(name = "users") 23 | @ToString(of = {"id", "name", "nickname", "email", "isDeleted"}) 24 | public class User extends BaseTimeEntity { 25 | 26 | @Id 27 | @GeneratedValue(strategy = GenerationType.IDENTITY) 28 | @Column(name = "user_id") 29 | private Long id; 30 | 31 | private String name; 32 | private String nickname; 33 | @Column(length = 100, nullable = false, unique = true) 34 | private String email; 35 | @Column(length = 200, nullable = false) 36 | private String password; 37 | 38 | @Column(name = "deleted", columnDefinition="bit default 0") 39 | private boolean isDeleted; 40 | @Column(name = "delete_date") 41 | private LocalDateTime deleteDate; 42 | 43 | @JsonIgnore 44 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) 45 | private List userGroups = new ArrayList<>(); 46 | 47 | @JsonIgnore 48 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) 49 | private List reservations = new ArrayList<>(); 50 | 51 | @JsonIgnore 52 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) 53 | private List comments = new ArrayList<>(); 54 | 55 | @JsonIgnore 56 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) 57 | private List likes = new ArrayList<>(); 58 | 59 | @Builder 60 | public User(Long id, String name, String nickname, String email, String password) { 61 | this.id = id; 62 | this.name = name; 63 | this.nickname = nickname; 64 | this.email = email; 65 | this.password = password; 66 | } 67 | 68 | public static User of(LoginDto loginDto) { 69 | return User.builder() 70 | .id(loginDto.getId()) 71 | .email(loginDto.getEmail()) 72 | .name(loginDto.getName()) 73 | .password(loginDto.getPassword()) 74 | .build(); 75 | } 76 | 77 | public static User of(SignUpRequest signUpRequest) { 78 | return User.builder() 79 | .email(signUpRequest.getEmail()) 80 | .name(signUpRequest.getName()) 81 | .nickname(signUpRequest.getNickname()) 82 | .password(encrypt(signUpRequest.getPassword())) 83 | .build(); 84 | } 85 | 86 | public void withdrawUser(boolean isDeleted) { 87 | this.isDeleted = isDeleted; 88 | this.deleteDate = LocalDateTime.now(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/naver/NaverClient.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.naver; 2 | 3 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchImageReq; 4 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchImageRes; 5 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchLocalReq; 6 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchLocalRes; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.core.ParameterizedTypeReference; 9 | import org.springframework.http.*; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.client.RestTemplate; 12 | import org.springframework.web.util.UriComponentsBuilder; 13 | 14 | import java.net.URI; 15 | 16 | @Component 17 | public class NaverClient { 18 | 19 | @Value("${naver.client.id}") 20 | private String naverClientId; 21 | 22 | @Value("${naver.client.secret}") 23 | private String naverClientSecret; 24 | 25 | @Value("${naver.url.search.local}") 26 | private String naverLocalSearchUrl; 27 | 28 | @Value("${naver.url.search.image}") 29 | private String naverImageSearchUrl; 30 | 31 | public SearchLocalRes searchLocal(SearchLocalReq searchLocalReq){ 32 | URI uri = UriComponentsBuilder.fromUriString(naverLocalSearchUrl) 33 | .queryParams(searchLocalReq.toMultiValueMap()) 34 | .build() 35 | .encode() 36 | .toUri(); 37 | 38 | HttpHeaders headers = new HttpHeaders(); 39 | headers.set("X-Naver-Client-Id", naverClientId); 40 | headers.set("X-Naver-Client-Secret", naverClientSecret); 41 | headers.setContentType(MediaType.APPLICATION_JSON); 42 | 43 | HttpEntity httpEntity = new HttpEntity<>(headers); 44 | ParameterizedTypeReference responseType = new ParameterizedTypeReference<>() { 45 | }; 46 | 47 | 48 | ResponseEntity responseEntity = new RestTemplate().exchange( 49 | uri, 50 | HttpMethod.GET, 51 | httpEntity, 52 | responseType 53 | ); 54 | 55 | return responseEntity.getBody(); 56 | } 57 | 58 | 59 | public SearchImageRes searchImage(SearchImageReq searchImageReq){ 60 | URI uri = UriComponentsBuilder.fromUriString(naverImageSearchUrl) 61 | .queryParams(searchImageReq.toMultiValueMap()) 62 | .build() 63 | .encode() 64 | .toUri(); 65 | 66 | HttpHeaders headers = new HttpHeaders(); 67 | headers.set("X-Naver-Client-Id", naverClientId); 68 | headers.set("X-Naver-Client-Secret", naverClientSecret); 69 | headers.setContentType(MediaType.APPLICATION_JSON); 70 | 71 | HttpEntity httpEntity = new HttpEntity<>(headers); 72 | ParameterizedTypeReference responseType = new ParameterizedTypeReference(){}; 73 | 74 | 75 | ResponseEntity responseEntity = new RestTemplate().exchange( 76 | uri, 77 | HttpMethod.GET, 78 | httpEntity, 79 | responseType 80 | ); 81 | 82 | return responseEntity.getBody(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/user/service/LoginServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.user.service; 2 | 3 | import com.dsg.wardstudy.common.exception.ErrorCode; 4 | import com.dsg.wardstudy.common.exception.WSApiException; 5 | import com.dsg.wardstudy.domain.user.dto.LoginDto; 6 | import com.dsg.wardstudy.domain.user.dto.SignUpRequest; 7 | import com.dsg.wardstudy.domain.user.dto.SignUpResponse; 8 | import com.dsg.wardstudy.domain.user.dto.UserInfo; 9 | import com.dsg.wardstudy.repository.user.UserRepository; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.log4j.Log4j2; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import javax.servlet.http.HttpSession; 16 | import java.util.Optional; 17 | 18 | @Log4j2 19 | @Service 20 | @RequiredArgsConstructor 21 | public class LoginServiceImpl implements LoginService{ 22 | 23 | private static final String USER_ID = "USER_ID"; 24 | public final HttpSession httpSession; 25 | private final UserRepository userRepository; 26 | private final UserService userService; 27 | 28 | @Override 29 | @Transactional 30 | public SignUpResponse signUp(SignUpRequest signUpRequest) { 31 | 32 | // user 중복 체크 33 | userRepository.findByEmail(signUpRequest.getEmail()) 34 | .ifPresent( u -> { 35 | throw new WSApiException(ErrorCode.DUPLICATED_ENTITY, "duplicate UserGroup"); 36 | }); 37 | UserInfo userInfo = userService.create(signUpRequest); 38 | log.info("signUp userInfo : {}", userInfo); 39 | // httpSession 에 담고 리턴 40 | httpSession.setAttribute(USER_ID, userInfo.getId()); 41 | // password 암호화하고 저장 42 | return SignUpResponse.mapToDto(userInfo); 43 | 44 | } 45 | 46 | @Override 47 | public void loginUser(LoginDto loginDto) { 48 | // 세션 값(다른 사람의 세션, 원래 없어야 함!)체크, 있으면 리턴 49 | if (this.isLoginUser()) { 50 | return; 51 | } 52 | // 없으면 비밀번호 체크 후 로그인 53 | LoginDto findLoginDto = userService.getByEmailAndPassword(loginDto.getEmail(), loginDto.getPassword()); 54 | log.info("loginUser findLoginDto: {}", findLoginDto); 55 | if (findLoginDto != null) { 56 | httpSession.setAttribute(USER_ID, findLoginDto.getId()); 57 | } else { 58 | throw new WSApiException(ErrorCode.NOT_FOUND_USER); 59 | } 60 | } 61 | 62 | @Override 63 | public void logoutUser() { 64 | // 세션 제거 65 | httpSession.removeAttribute(USER_ID); 66 | log.info("session 제거"); 67 | } 68 | 69 | /** 70 | * 로그인 확인 여부 71 | * @return 확인여부 boolean 72 | */ 73 | @Override 74 | public boolean isLoginUser() { 75 | Long userId = (Long) httpSession.getAttribute(USER_ID); 76 | log.info("isLoginUser, userId: {}", userId); 77 | return userId != null; 78 | } 79 | 80 | @Override 81 | public Long getUserId() { 82 | Long userId = Optional.ofNullable(httpSession.getAttribute(USER_ID)) 83 | .map(i -> (Long) i) 84 | .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER)); 85 | log.info("loginService getUserId userId: {}", userId); 86 | return userId; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/repository/user/UserGroupRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.user; 2 | 3 | import com.dsg.wardstudy.domain.reservation.entity.Room; 4 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 5 | import com.dsg.wardstudy.domain.user.entity.User; 6 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 7 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 8 | import com.dsg.wardstudy.domain.user.constant.UserType; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 14 | 15 | import java.util.List; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @Slf4j 20 | @DataJpaTest 21 | //@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 22 | class UserGroupRepositoryTest { 23 | 24 | @Autowired 25 | private UserRepository userRepository; 26 | @Autowired 27 | private UserGroupRepository userGroupRepository; 28 | 29 | @Autowired 30 | private StudyGroupRepository studyGroupRepository; 31 | 32 | private User user; 33 | private UserGroup userGroup; 34 | private StudyGroup studyGroup; 35 | private Room room; 36 | 37 | @BeforeEach 38 | void setUp() { 39 | user = User.builder() 40 | .email("dsgfunk@gmail.com") 41 | .build(); 42 | 43 | userGroup = UserGroup.builder() 44 | .userType(UserType.PARTICIPANT) 45 | .build(); 46 | 47 | studyGroup = StudyGroup.builder() 48 | .title("title_") 49 | .content("study 번 방") 50 | .build(); 51 | 52 | room = Room.builder() 53 | .name("roomA") 54 | .build(); 55 | 56 | } 57 | 58 | @Test 59 | public void givenUserIdAndSGId_whenFindById_thenReturnUserType() { 60 | // given - precondition or setup 61 | User savedUser = userRepository.save(user); 62 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 63 | UserGroup savedUserGroup = userGroupRepository.save(userGroup); 64 | // when - action or the behaviour that we are going test 65 | UserType userType = userGroupRepository.findUserTypeByUserIdAndSGId(savedUser.getId(), savedStudyGroup.getId()).get(); 66 | log.info("userType: {}", userType); 67 | // then - verify the output 68 | assertThat(userType).isNotNull(); 69 | assertThat(userType).isEqualTo(savedUserGroup.getUserType()); 70 | 71 | } 72 | 73 | @Test 74 | public void givenStudyGroupId_whenFindById_thenUserList(){ 75 | // given - precondition or setup 76 | User savedUser = userRepository.save(user); 77 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 78 | UserGroup savedUserGroup = userGroupRepository.save(userGroup); 79 | // when - action or the behaviour that we are going test 80 | List users = userGroupRepository.findUserBySGId(savedStudyGroup.getId()); 81 | // then - verify the output 82 | log.info("users: {}", users); 83 | assertThat(users).isNotNull(); 84 | assertThat(users.size()).isEqualTo(2); 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any // 사용 가능한 에이전트에서 이 파이프라인 또는 해당 단계를 실행 3 | // git 프로젝트 credentials 는 access token 으로 사용 4 | stages { 5 | stage('Prepare') { 6 | steps { 7 | git branch: 'develop', 8 | url: 'https://github.com/mooh2jj/ward-study-reservation.git' 9 | } 10 | 11 | post { 12 | success { 13 | sh 'echo "Successfully Cloned Repository"' 14 | } 15 | failure { 16 | sh 'echo "Fail Cloned Repository"' 17 | } 18 | } 19 | } 20 | 21 | stage('Build') { 22 | steps { 23 | // gralew이 있어야됨. git clone해서 project를 가져옴. 24 | sh 'chmod +x gradlew' 25 | sh './gradlew --warning-mode=all --stacktrace clean build -x test' 26 | 27 | 28 | sh 'ls -al ./build' 29 | } 30 | post { 31 | success { 32 | echo 'gradle build success' 33 | } 34 | 35 | failure { 36 | echo 'gradle build failed' 37 | } 38 | } 39 | } 40 | stage('Test') { 41 | steps { 42 | echo '테스트 단계와 관련된 몇 가지 단계를 수행합니다.' 43 | } 44 | } 45 | stage('Prune Docker data') { 46 | steps { 47 | sh 'echo "Prune Docker data"' 48 | sh 'docker system prune -a --volumes -f' 49 | } 50 | 51 | post { 52 | success { 53 | sh 'echo "Prune Docker data Success"' 54 | } 55 | failure { 56 | sh 'echo "Prune Docker data Fail"' 57 | } 58 | } 59 | } 60 | 61 | stage('Docker Build'){ 62 | steps{ 63 | sh 'echo " Image Bulid Start"' 64 | sh 'docker build . -t mooh2jj/ward-study-reaservation' 65 | } 66 | post { 67 | success { 68 | sh 'echo "Bulid Docker Image Success"' 69 | } 70 | 71 | failure { 72 | sh 'echo "Bulid Docker Image Fail"' 73 | } 74 | } 75 | } 76 | 77 | stage('Docker Push') { 78 | steps { 79 | withCredentials([string(credentialsId: 'dockerHubPwd', variable: 'dockerHubPwd')]) { 80 | sh "docker login -u mooh2jj -p ${dockerHubPwd}" 81 | } 82 | sh 'docker push mooh2jj/ward-study-reaservation' 83 | } 84 | 85 | post { 86 | success { 87 | echo 'Docker Push success' 88 | } 89 | 90 | failure { 91 | echo 'Docker Push failed' 92 | } 93 | } 94 | } 95 | stage('Docker Deploy'){ 96 | steps{ 97 | sh 'docker-compose up -d' 98 | sh 'docker-compose ps' 99 | } 100 | post { 101 | success { 102 | echo 'docker-compose success' 103 | } 104 | 105 | failure { 106 | echo 'docker-compose failed' 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/controller/CommentController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.controller; 2 | 3 | import com.dsg.wardstudy.common.auth.AuthUser; 4 | import com.dsg.wardstudy.domain.comment.dto.CommentDto; 5 | import com.dsg.wardstudy.domain.comment.service.CommentService; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.log4j.Log4j2; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import javax.validation.Valid; 13 | import java.util.List; 14 | 15 | @Log4j2 16 | @RestController 17 | @RequiredArgsConstructor 18 | public class CommentController { 19 | 20 | private final CommentService commentService; 21 | 22 | // 댓글 등록 23 | @PostMapping("/study-group/{studyGroupId}/comments") 24 | public ResponseEntity createComment( 25 | @PathVariable Long studyGroupId, 26 | @Valid @RequestBody CommentDto commentDto, 27 | @AuthUser Long userId 28 | ) { 29 | log.info("createComment run, studyGroupId: {}, commentDto: {}", studyGroupId, commentDto); 30 | log.info("createComment userId: {}", userId); 31 | return new ResponseEntity<>(commentService.createComment(studyGroupId, userId, commentDto), HttpStatus.CREATED); 32 | } 33 | 34 | // 특정스터디그룹 댓글 리스트 가져오기 35 | @GetMapping("/study-group/{studyGroupId}/comments") 36 | public List getCommentsByStudyGroupId( 37 | @PathVariable Long studyGroupId 38 | ) { 39 | log.info("getCommentsByStudyGroupId run, studyGroupId: {}", studyGroupId); 40 | return commentService.getCommentsByStudyGroupId(studyGroupId); 41 | } 42 | 43 | // 특정 댓글 1개 상세보기 44 | @GetMapping("/study-group/{studyGroupId}/comments/{commentId}") 45 | public CommentDto getCommentById( 46 | @PathVariable Long studyGroupId, 47 | @PathVariable Long commentId, 48 | @AuthUser Long userId 49 | ) { 50 | log.info("getCommentById run, studyGroupId: {}, commentId: {}", studyGroupId, commentId); 51 | log.info("getCommentById userId: {}", userId); 52 | return commentService.getCommentById(studyGroupId, userId, commentId); 53 | } 54 | 55 | // 댓글 수정 56 | @PutMapping("/study-group/{studyGroupId}/comments/{commentId}") 57 | public ResponseEntity updateComment( 58 | @PathVariable Long studyGroupId, 59 | @PathVariable Long commentId, 60 | @Valid @RequestBody CommentDto commentDto, 61 | @AuthUser Long userId 62 | ) { 63 | log.info("updateComment run, studyGroupId: {}, commentId: {}, commentDto: {}", 64 | studyGroupId, commentId, commentDto); 65 | log.info("updateComment userId: {}", userId); 66 | return ResponseEntity.ok(commentService.updateComment(studyGroupId, userId, commentId, commentDto)); 67 | } 68 | 69 | // 댓글 삭제 70 | @DeleteMapping("/study-group/{studyGroupId}/comments/{commentId}") 71 | public ResponseEntity deleteComment( 72 | @PathVariable Long studyGroupId, 73 | @PathVariable Long commentId, 74 | @AuthUser Long userId 75 | ) { 76 | log.info("deleteComment run, studyGroupId: {}, commentId: {}", studyGroupId, commentId); 77 | log.info("deleteComment userId: {}", userId); 78 | commentService.deleteComment(studyGroupId, userId, commentId); 79 | return new ResponseEntity<>("comment deleteAll successfully.", HttpStatus.OK); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/wish/service/WishServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.wish.service; 2 | 3 | import com.dsg.wardstudy.common.exception.ErrorCode; 4 | import com.dsg.wardstudy.common.exception.WSApiException; 5 | import com.dsg.wardstudy.domain.wish.dto.WishDto; 6 | import com.dsg.wardstudy.domain.wish.entity.Wish; 7 | import com.dsg.wardstudy.domain.wish.naver.NaverClient; 8 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchImageReq; 9 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchImageRes; 10 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchLocalReq; 11 | import com.dsg.wardstudy.domain.wish.naver.dto.SearchLocalRes; 12 | import com.dsg.wardstudy.repository.wish.WishRepository; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | 21 | @Slf4j 22 | @Service 23 | @RequiredArgsConstructor 24 | public class WishServiceImpl implements WishService { 25 | 26 | private final NaverClient naverClient; 27 | private final WishRepository wishRepository; 28 | 29 | 30 | @Transactional 31 | @Override 32 | public WishDto search(String query) { 33 | // 지역 검색 -> 이미지 검색 -> 결과 리턴 순 34 | // 지역 검색 35 | SearchLocalReq searchLocalReq = new SearchLocalReq(); 36 | searchLocalReq.setQuery(query); 37 | SearchLocalRes searchLocalRes = naverClient.searchLocal(searchLocalReq); 38 | 39 | if (searchLocalRes.getTotal() > 0) { 40 | SearchLocalRes.SearchLocalItem localItem = searchLocalRes.getItems().stream().findFirst().get(); 41 | 42 | String imageQuery = localItem.getTitle().replaceAll("<[^>]*>", ""); // 괄호 다 삭제 43 | 44 | SearchImageReq searchImageReq = new SearchImageReq(); 45 | searchImageReq.setQuery(imageQuery); 46 | 47 | // 이미지 검색 48 | SearchImageRes searchImageRes = naverClient.searchImage(searchImageReq); 49 | 50 | if (searchImageRes.getTotal() > 0) { 51 | 52 | SearchImageRes.SearchImageItem imageItem = searchImageRes.getItems().stream().findFirst().get(); 53 | 54 | // 결과 리턴 55 | return WishDto.builder() 56 | .title(localItem.getTitle()) 57 | .category(localItem.getCategory()) 58 | .address(localItem.getAddress()) 59 | .roadAddress(localItem.getRoadAddress()) 60 | .homePageLink(localItem.getLink()) 61 | .imageLink(imageItem.getLink()) 62 | .build(); 63 | } 64 | 65 | } 66 | 67 | return new WishDto(); 68 | } 69 | 70 | 71 | @Transactional 72 | @Override 73 | public WishDto add(WishDto wishDto) { 74 | 75 | Wish savedWish = wishRepository.save(Wish.of(wishDto)); 76 | log.info("savedWish: {}", savedWish); 77 | 78 | return WishDto.mapToDto(savedWish); 79 | } 80 | 81 | @Transactional(readOnly = true) 82 | @Override 83 | public List getAll() { 84 | return wishRepository.findAll().stream() 85 | .map(WishDto::mapToDto) 86 | .collect(Collectors.toList()); 87 | } 88 | 89 | @Transactional 90 | @Override 91 | public void delete(Long wishId) { 92 | wishRepository.deleteById(wishId); 93 | } 94 | 95 | @Transactional 96 | @Override 97 | public void addVisit(Long wishId) { 98 | Wish findWish = wishRepository.findById(wishId) 99 | .orElseThrow(() -> new WSApiException(ErrorCode.NOT_FOUND_USER, "no wish")); 100 | 101 | findWish.addVisit(); 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/comment/service/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.comment.service; 2 | 3 | import com.dsg.wardstudy.common.exception.ErrorCode; 4 | import com.dsg.wardstudy.common.exception.WSApiException; 5 | import com.dsg.wardstudy.domain.comment.entity.Comment; 6 | import com.dsg.wardstudy.domain.comment.dto.CommentDto; 7 | import com.dsg.wardstudy.domain.comment.repository.CommentRepository; 8 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 9 | import com.dsg.wardstudy.domain.user.entity.User; 10 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 11 | import com.dsg.wardstudy.repository.user.UserRepository; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | public class CommentServiceImpl implements CommentService{ 22 | 23 | private final CommentRepository commentRepository; 24 | private final StudyGroupRepository studyGroupRepository; 25 | 26 | private final UserRepository userRepository; 27 | 28 | @Transactional 29 | @Override 30 | public CommentDto createComment(Long studyGroupId, Long userId, CommentDto commentDto) { 31 | 32 | StudyGroup findStudyGroup = studyGroupRepository.findById(studyGroupId) 33 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY, "no studyGroup")); 34 | User findUser = userRepository.findById(userId) 35 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY, "no user")); 36 | 37 | commentDto.crateUserInfo(findUser); 38 | Comment ofComment = Comment.of(commentDto); 39 | ofComment.setStudyGroup(findStudyGroup); 40 | ofComment.setUser(findUser); 41 | Comment savedComment = commentRepository.save(ofComment); 42 | 43 | return CommentDto.mapToDto(savedComment); 44 | } 45 | 46 | @Transactional(readOnly = true) 47 | @Override 48 | public List getCommentsByStudyGroupId(Long studyGroupId) { 49 | 50 | List comments = commentRepository.findByStudyGroupId(studyGroupId); 51 | return comments.stream() 52 | .map(CommentDto::mapToDto) 53 | .collect(Collectors.toList()); 54 | } 55 | 56 | @Transactional(readOnly = true) 57 | @Override 58 | public CommentDto getCommentById(Long studyGroupId, Long userId, Long commentId) { 59 | 60 | Comment findComment = validateComment(studyGroupId, userId, commentId); 61 | return CommentDto.mapToDto(findComment); 62 | } 63 | 64 | 65 | 66 | @Transactional 67 | @Override 68 | public CommentDto updateComment(Long studyGroupId, Long userId, Long commentId, CommentDto commentDto) { 69 | Comment findComment = validateComment(studyGroupId, userId, commentId); 70 | 71 | findComment.updateComment(commentDto); 72 | return CommentDto.mapToDto(findComment); 73 | } 74 | 75 | @Transactional 76 | @Override 77 | public void deleteComment(Long studyGroupId, Long userId, Long commentId) { 78 | Comment findComment = validateComment(studyGroupId, userId, commentId); 79 | 80 | commentRepository.deleteById(findComment.getId()); 81 | } 82 | 83 | private Comment validateComment(Long studyGroupId, Long userId, Long commentId) { 84 | StudyGroup findStudyGroup = studyGroupRepository.findById(studyGroupId) 85 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY, "no studyGroup")); 86 | 87 | User findUser = userRepository.findById(userId) 88 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY, "no user")); 89 | 90 | Comment findComment = commentRepository.findById(commentId) 91 | .orElseThrow(() -> new WSApiException(ErrorCode.NO_FOUND_ENTITY, "no comment")); 92 | 93 | return findComment; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/attach/controller/AttachController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.attach.controller; 2 | 3 | import com.dsg.wardstudy.domain.attach.dto.AttachDTO; 4 | import com.dsg.wardstudy.domain.attach.service.AttachService; 5 | import com.google.cloud.storage.Blob; 6 | import com.google.cloud.storage.Bucket; 7 | import com.google.firebase.cloud.StorageClient; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.transaction.annotation.Transactional; 14 | import org.springframework.web.bind.annotation.*; 15 | import org.springframework.web.multipart.MultipartFile; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.nio.file.Files; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.UUID; 25 | 26 | @Log4j2 27 | @RequiredArgsConstructor 28 | @RestController 29 | public class AttachController { 30 | 31 | private final AttachService attachService; 32 | 33 | @Value("${app.firebase-bucket}") 34 | private String uploadPath; 35 | 36 | /** 37 | * 스터디그룹 게시글 당 파일첨부 리스트 가져오기 38 | * @param studyGroupId 스터디그룹 게시글 seq 39 | * @return ResponseBody in 파일첨부 리스트 40 | */ 41 | @GetMapping("/attach/list/{studyGroupId}") 42 | public ResponseEntity> getAttachList( 43 | @PathVariable("studyGroupId") Long sgId) { 44 | 45 | log.info("getAttachList sgId: {}", sgId); 46 | return new ResponseEntity<>(attachService.getAttachList(sgId), HttpStatus.OK); 47 | } 48 | 49 | 50 | /** 51 | * (다중)파일업로드 52 | * @param uploadFile (다중)파일 53 | * @return 첨부파일 리스트 54 | */ 55 | @PostMapping("/attach/upload") 56 | public ResponseEntity> uploadAjaxPost(@RequestParam("file") List uploadFile) { 57 | 58 | List attachList = new ArrayList<>(); 59 | 60 | for (MultipartFile multipartFile : uploadFile) { 61 | 62 | String uploadFileName = multipartFile.getOriginalFilename(); 63 | long size = multipartFile.getSize(); 64 | log.info("-------------------------------------"); 65 | log.info("Upload File Name: " + uploadFileName); 66 | log.info("Upload File Size: " + size); 67 | 68 | AttachDTO attachDTO = new AttachDTO(); 69 | 70 | uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf(File.separator) + 1); 71 | log.info("only file name: " + uploadFileName); 72 | attachDTO.setFileName(uploadFileName); 73 | 74 | UUID uuid = UUID.randomUUID(); 75 | 76 | uploadFileName = uuid.toString() + "_" + uploadFileName; 77 | 78 | try { 79 | File saveFile = new File(uploadPath, uploadFileName); 80 | // multipartFile.transferTo(saveFile); 81 | Bucket bucket = StorageClient.getInstance().bucket(uploadPath); 82 | InputStream content = new ByteArrayInputStream(multipartFile.getBytes()); 83 | Blob blob = bucket.create(uploadFileName , content, multipartFile.getContentType()); 84 | log.info("create blob: {}", blob); 85 | attachDTO.setUuid(uuid.toString()); 86 | attachDTO.setUploadPath(uploadPath); 87 | 88 | if (checkImageType(saveFile)) { 89 | attachDTO.setImage(true); 90 | } 91 | 92 | attachList.add(attachDTO); 93 | } catch (Exception e) { 94 | log.error(e.getMessage()); 95 | } // end catch 96 | } // end for 97 | return new ResponseEntity<>(attachList, HttpStatus.OK); 98 | } 99 | 100 | /** 101 | * 이미지 파일인지 유무 판단 메서드 102 | * @param file 103 | * @return boolean 104 | */ 105 | private boolean checkImageType(File file) { 106 | try { 107 | String contentType = Files.probeContentType(file.toPath()); 108 | log.info("checkImageType: {}", contentType); 109 | return contentType.startsWith("image"); 110 | } catch (IOException e) { 111 | e.printStackTrace(); 112 | } 113 | return false; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👯‍♀️ ward-study-reservation 2 | 스터디모임 룸 예약 서비스 3 | ![ward-study_image](https://user-images.githubusercontent.com/62453668/169026026-6278161e-781f-4f9c-86fd-1d7a0e8c821f.jpg) 4 | 5 | 언제 어디서든 소규모로 사용자들이 스터디모임을 활성화하기 위한 서비스로 도안하였습니다. 6 | 스터디를 형성하는 데 많은 갈증을 느끼는 사용자가 하나의 토픽을 중심으로 룸을 예약하여 “몰입의 경험”을 선사하는 것이 이 서비스의 취지입니다. 7 | 8 |
9 | 10 | ## 🔍 서비스 `MVP(Minimum Valuable Product)` 11 | 12 | **1. 사용자(리더)가 StudyGroup 등록/참여** 13 | 14 | **2. 사용자(리더)가 Reservation 등록** 15 | 16 | **3. 사용자가 참가한 StudyGroup의 Reservation 조회** 17 | 18 | **4. 해당 Room의 Reservation 조회** 19 | 20 | **5. Reservation의 해당 사용자들에게 Notification 이메일 정기알람** 21 | 22 |
23 | 24 | ## 💡 프로젝트 목표 25 | **1. 객체 지향 원리를 적용하여 `CleanCode`를 목표로 유지보수가 용이한 코드 구현** 26 | - 중복되는 코드들, 불필요하게 수정이 일어날 코드들을 최소화해 확장이 용이하게 노력합니다. 27 | - SOLID 원칙과 디자인패턴의 이해를 바탕을 하여 최대한 `도메인 주도 설계`를 하기 위해 노력합니다. 28 | 29 | **2. 단순 기능 구현만이 아닌 대용량 트래픽을 고려한 `scale-out`에 고려한 서버 구조 설계** 30 | - 서비스가 성장하여 대량의 TPS 수치의 접속자수가 있다고 가정, 그 정도의 트래픽에도 견딜 수 있는 `분산 데이터베이스 아키텍쳐`를 설계하기 위해 노력했습니다. 31 | 32 | **3. `문서화`를 통한 협업** 33 | - 프론트엔드-백엔드 팀들이 협업하는 환경에서 API 문서를 통한 커퓨니케이션 호율성을 높이기 위해 노력합니다. 34 | - API 문서를 함께 작성하는 것은 비효율적이기 때문에 `Swagger`와 같은 툴을 활용하여 문서화와 테스트도 쉽게 처리할 수 있게 합니다. 35 | 36 | **4. `테스트코드`를 통해 코드품질 향상, `CI/CD`를 통한 자동화 구현** 37 | - 다수의 개발자가 하나의 서비스를 개발해나가는 환경에서는 각자의 코드가 쉽게 배포할 수 있도록 Jenkins를 활용해 배포 자동화하는 설계에도 많은 리소스를 소요하였습니다. 38 | - CI/CD를 직접 구축하여 애자일한 개발 프로세스를 실현하기 위해 노력합니다. 39 | 40 | **5. `성능 모니터링`으로 프로젝트의 신뢰성을 높임** 41 | - `nGrinder`나 `JMeter` 같은 툴을 사용해 TPS 같은 성능지수를 체크, 트래픽이 많이 발생하는 부분을 모니터링하여 개선점을 찾아냅니다. 42 | - 그외 로그수집을 하여 개발 오류 파트를 빠르게 찾기 위해 `ELK` 시스템을 구축하여, 애플리케이션 오류 진단, 인프라 모니터링에 최적화시키기 위해 노력합니다. 43 | 44 |
45 | 46 | ## 🛒 사용 기술 스택 47 | - Java11 48 | - SpringBoot2.6 49 | - Gradle7.4 50 | - MySQL8.0 / Redis 51 | - JPA / QueryDsl 52 | - JUnit5 53 | - Spring Batch 54 | - Docker 55 | - Jenkins 56 | - JMeter 57 | 58 |
59 | 60 | ## 🔗 CI/CD 구조도 61 | ![image](https://user-images.githubusercontent.com/62453668/164407464-9df1d184-da84-4e4f-b533-2aad2a5b3757.png) 62 | 63 |
64 | 65 | ## 🎡 서버 구조도 66 | ![image](https://user-images.githubusercontent.com/62453668/204154448-c269d0bc-588c-4c4a-a646-88c9d54d9ba2.png) 67 | 68 |
69 | 70 | ## 🗃 API 명세서 71 | ✳ https://documenter.getpostman.com/view/14757100/2s8YzMY5fG 72 | 73 |
74 | 75 | ## 👁‍🗨 이슈 정리 76 | [Wiki Issues & Trouble shooting](https://github.com/f-lab-edu/ward-study-reservation/wiki/4.-Issues-&-Trouble-shooting)에서 확인할 수 있습니다! 77 | 78 | - [예약 시스템에서 동시성 제어와 더블부킹(중복요청)은 어떻게 막을까?](https://velog.io/@mooh2jj/예약-시스템에서-동시성-제어와-더블부킹중복요청은-어떻게-막을까) 79 | - [배치 시스템 Cursor 기반 vs Paging 기반 ItemReader 정하기](https://velog.io/@mooh2jj/Cursor-기반-vs-Paging-기반-ItemReader-정하기) 80 | - [Redis 캐싱적용해서 read 작업 성능 개선하기](https://velog.io/@mooh2jj/Redis-캐싱적용해서-read-작업-성능-개선하기) 81 | - [서버 분산 처리 환경에서 데이터의 불일치 문제- Redis에 session 저장하기](https://velog.io/@mooh2jj/서버-분산-처리-환경에서-데이터의-불일치-Redis에-session-저장하기) 82 | - [Spring ArgumentResolver 로 인증처리하기](https://velog.io/@mooh2jj/Spring-ArgumentResolver-로-인증처리하기) 83 | - [MySQL 실행계획으로 성능 측정하기(인덱스 설정 이후)](https://velog.io/@mooh2jj/MySQL-실행계획으로-성능-측정하기인덱스-설정-이후) 84 | - [JMeter로 성능 테스트](https://velog.io/@mooh2jj/JMeter로-성능-테스트) 85 | 86 | 등등 ... 87 | 88 |
89 | 90 | ## 🔖 Git-Flow 브랜치 및 PR 전략 91 | 92 | ✳ 참고문헌 : 93 | 우아한 형제들 기술블로그 [우린 Git-flow를 사용하고 있어요](https://woowabros.github.io/experience/2017/10/30/baemin-mobile-git-branch-strategy.html) 94 | 95 | 96 | ![image](https://user-images.githubusercontent.com/62453668/169458249-74e55a36-a631-4440-a52d-332fa78eb9f4.png) 97 | 98 | 99 | ✅ `master` : 제품으로 출시될 수 있는 브랜치를 의미합니다. 100 | 101 | ✅ `develop` : 다음 출시 버전을 개발하는 브랜치입니다. feature에서 리뷰완료한 브랜치를 Merge하고 있습니다. 102 | 103 | ✅ `feature` : 기능을 개발하는 브랜치 104 | 105 | ✅ `release` : 이번 출시 버전을 준비하는 브랜치 106 | 107 | ✅ `hotfix` : 출시 버전에서 발생한 버그를 수정하는 브랜치 108 | 109 | 110 | 111 | - `master`를 항상 최신 상태로 만들며, stable 상태로 Product에 배포되는 브랜치 (master = main) 로 삼습니다. 112 | - 신규개발 건은 `develop` 을 base로 `feature/#이슈번호` or `feature/작업명` 의 브랜치명으로 생성 후 작업한 다음 `PR`을 날립니다. 113 | - 아직 개발 진행 중이라면 `In Progress` 라벨을 달고, 코드리뷰가 필요한 경우 `Asking for Review` 라벨을 답니다. 리뷰 후 리팩토링이 필요하다면 추가로 `refactoring` 라벨을 달아 진행합니다. 114 | - 모든 `PR`은 반드시 지정한 `리뷰어`에게 코드리뷰를 받아야만 합니다. 115 | - 코드리뷰어의` Approve` 를 받아야 `Merge pull request` 를 할 수 있습니다. 116 | 117 | 118 | 119 |
120 | 121 | ## 🎞 ER 다이어그램 122 | ![image](https://user-images.githubusercontent.com/62453668/221393510-564ee139-48f6-4d2b-9a71-68fd8e5defb5.png) 123 | 124 |
125 | 126 | ## 🎨 클라이언트 화면 127 | ![image](https://user-images.githubusercontent.com/62453668/204130495-55216181-3106-4f9f-9af1-65c4a7ba8403.png) 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/InsertTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy; 2 | 3 | import com.dsg.wardstudy.common.utils.Encryptor; 4 | import com.dsg.wardstudy.config.jpa.JpaAuditingConfig; 5 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 6 | import com.dsg.wardstudy.domain.reservation.entity.Room; 7 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 8 | import com.dsg.wardstudy.domain.user.constant.UserType; 9 | import com.dsg.wardstudy.domain.user.entity.User; 10 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 11 | import com.dsg.wardstudy.repository.reservation.ReservationRepository; 12 | import com.dsg.wardstudy.repository.reservation.RoomRepository; 13 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 14 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 15 | import com.dsg.wardstudy.repository.user.UserRepository; 16 | import com.namics.commons.random.RandomData; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.Test; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 22 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 23 | import org.springframework.context.annotation.Import; 24 | import org.springframework.test.annotation.Rollback; 25 | import org.springframework.test.context.ActiveProfiles; 26 | import org.springframework.util.StopWatch; 27 | 28 | import java.time.LocalDateTime; 29 | import java.util.stream.LongStream; 30 | 31 | @Slf4j 32 | @Import(JpaAuditingConfig.class) 33 | @ActiveProfiles("test") 34 | @DataJpaTest 35 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 36 | @Rollback(value = false) 37 | public class InsertTest { 38 | 39 | @Autowired 40 | private ReservationRepository reservationRepository; 41 | 42 | @Autowired 43 | private StudyGroupRepository studyGroupRepository; 44 | 45 | @Autowired 46 | private UserRepository userRepository; 47 | @Autowired 48 | private UserGroupRepository userGroupRepository; 49 | 50 | @Autowired 51 | private RoomRepository roomRepository; 52 | 53 | private User user; 54 | private UserGroup userGroup; 55 | private StudyGroup studyGroup; 56 | private Room room; 57 | 58 | @BeforeEach 59 | void setUp() { 60 | user = User.builder() 61 | .email("dsgfunk@gmail.com") 62 | .password(Encryptor.encrypt("1234")) 63 | .build(); 64 | 65 | userGroup = UserGroup.builder() 66 | .userType(UserType.LEADER) 67 | .build(); 68 | 69 | studyGroup = StudyGroup.builder() 70 | .title("title_") 71 | .content("title_study 번 방") 72 | .build(); 73 | 74 | room = Room.builder() 75 | .name("roomA") 76 | .build(); 77 | 78 | userRepository.save(user); 79 | userGroupRepository.save(userGroup); 80 | studyGroupRepository.save(studyGroup); 81 | roomRepository.save(room); 82 | } 83 | 84 | @Test 85 | public void test() { 86 | 87 | StopWatch stopWatch = new StopWatch(); 88 | stopWatch.start(); 89 | 90 | LongStream.rangeClosed(1, 28*10000).forEach( i -> { 91 | Reservation reservation = Reservation.builder() 92 | .startTime(random()) 93 | .endTime(random()) 94 | .user(user) 95 | .studyGroup(studyGroup) 96 | .room(room) 97 | .build(); 98 | 99 | reservationRepository.save(reservation); 100 | 101 | } 102 | ); 103 | stopWatch.stop(); 104 | log.info("객체 생성 시간: {}", stopWatch.getTotalTimeSeconds()); 105 | 106 | } 107 | 108 | @Test 109 | public void test2() { 110 | 111 | StopWatch stopWatch = new StopWatch(); 112 | stopWatch.start(); 113 | 114 | LongStream.rangeClosed(1, 28*10000).forEach( i -> { 115 | StudyGroup studyGroup = StudyGroup.builder() 116 | .title("test_title_"+i) 117 | .content("test_content_"+i) 118 | .build(); 119 | studyGroupRepository.save(studyGroup); 120 | 121 | } 122 | ); 123 | stopWatch.stop(); 124 | log.info("객체 생성 시간: {}", stopWatch.getTotalTimeSeconds()); 125 | 126 | } 127 | 128 | public LocalDateTime random() { 129 | LocalDateTime now = LocalDateTime.now(); 130 | int year = 60 * 60 * 24 * 365; 131 | return now.plusSeconds((long) RandomData.randomInteger(-2 * year, 2 * year)); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/reservation/controller/ReservationController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.reservation.controller; 2 | 3 | import com.dsg.wardstudy.common.auth.AuthUser; 4 | import com.dsg.wardstudy.domain.reservation.dto.ReservationCommand; 5 | import com.dsg.wardstudy.domain.reservation.dto.ReservationDetails; 6 | import com.dsg.wardstudy.domain.reservation.service.ReservationService; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.log4j.Log4j2; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | 15 | @Log4j2 16 | @RestController 17 | @RequiredArgsConstructor 18 | public class ReservationController { 19 | 20 | private final ReservationService reservationService; 21 | 22 | // 리더일때만 예약 등록, 수정 가능 23 | // 예약 등록 24 | @PostMapping("/study-group/{studyGroupId}/room/{roomId}/reservation") 25 | public ResponseEntity register( 26 | @PathVariable("studyGroupId") Long studyGroupId, 27 | @PathVariable("roomId") Long roomId, 28 | @RequestBody ReservationCommand.RegisterReservation registerReservation, 29 | @AuthUser Long userId 30 | ) throws Exception { 31 | log.info("reservation register, studyGroupId: {}, roomId: {}, request: {}", 32 | studyGroupId, 33 | roomId, 34 | registerReservation); 35 | log.info("reservation register, userId: {}", userId); 36 | registerReservation.setUserId(userId); 37 | 38 | return new ResponseEntity<>(reservationService.register( 39 | studyGroupId, roomId, registerReservation), HttpStatus.CREATED); 40 | } 41 | 42 | // 등록한 예약 상세 보기 43 | @GetMapping("/room/{roomId}/reservation/{reservationToken}") 44 | public ResponseEntity getReservation( 45 | @PathVariable("roomId") Long roomId, 46 | @PathVariable("reservationToken") String reservationToken, 47 | @AuthUser Long userId 48 | ) { 49 | log.info("reservation getById, roomId: {}, reservationToken: {}", roomId, reservationToken); 50 | log.info("reservation getByIds, userId: {}", userId); 51 | return ResponseEntity.ok(reservationService.getByRoomIdAndReservationToken(roomId, reservationToken)); 52 | } 53 | 54 | // 해당 룸 예약 조회 startTime & endTime(mandatory) 55 | @GetMapping("/room/{roomId}/reservation/query") 56 | public ResponseEntity> getByRoomIdAndTimePeriod( 57 | @PathVariable("roomId") Long roomId, 58 | @RequestParam(value = "startTime") String startTime, 59 | @RequestParam(value = "endTime") String endTime, 60 | @AuthUser Long userId 61 | ) { 62 | log.info("reservation getByRoomIdAndTime, roomId: {}, startTime: {}, endTime: {}", roomId, startTime, endTime); 63 | log.info("reservation getByRoomIdAndTimePeriod, userId: {}", userId); 64 | return ResponseEntity.ok(reservationService.getByRoomIdAndTimePeriod(roomId, startTime, endTime)); 65 | } 66 | 67 | // 해당 룸 예약 조회 startTime & endTime x 68 | @GetMapping("/room/{roomId}/reservation") 69 | public ResponseEntity> getByRoomId( 70 | @PathVariable("roomId") Long roomId, 71 | @AuthUser Long userId 72 | 73 | ) { 74 | log.info("reservation getByRoomId, roomId: {}", roomId); 75 | log.info("reservation getByRoomId, userId: {}", userId); 76 | return ResponseEntity.ok(reservationService.getByRoomId(roomId)); 77 | } 78 | 79 | 80 | // 해당 유저 예약 조회 81 | @GetMapping("/reservation") 82 | public ResponseEntity> getAllByUserId( 83 | @AuthUser Long userId 84 | ) { 85 | log.info("reservation getAllByUserId, userId: {}", userId); 86 | return ResponseEntity.ok(reservationService.getAllByUserId(userId)); 87 | } 88 | 89 | // 예약 수정 90 | @PutMapping("/room/{roomId}/reservation/{reservationToken}") 91 | public String updateReservation( 92 | @PathVariable("roomId") Long roomId, 93 | @PathVariable("reservationToken") String reservationToken, 94 | @RequestBody ReservationCommand.UpdateReservation updateReservation, 95 | @AuthUser Long userId 96 | 97 | ) { 98 | log.info("reservation updateById, roomId: {}, reservationToken: {}", roomId, reservationToken); 99 | log.info("reservation updateById, userId: {}", userId); 100 | updateReservation.setUserId(userId); 101 | return reservationService.updateByToken(roomId, reservationToken, updateReservation); 102 | } 103 | 104 | // 예약 삭제 105 | @DeleteMapping("/reservation/{reservationToken}") 106 | public ResponseEntity deleteReservation( 107 | @PathVariable("reservationToken") String reservationToken, 108 | @AuthUser Long userId 109 | ) { 110 | log.info("reservation deleteById, reservationToken: {}" ,reservationToken); 111 | log.info("reservation deleteById, userId: {}", userId); 112 | reservationService.deleteByToken(userId, reservationToken); 113 | return new ResponseEntity<>("a reservation successfully deleted!", HttpStatus.OK); 114 | } 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/config/batch/NotificationAlarmJobConfig.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.config.batch; 2 | 3 | 4 | import com.dsg.wardstudy.common.adapter.mail.MailMessageGenerator; 5 | import com.dsg.wardstudy.common.adapter.mail.MailSendService; 6 | import com.dsg.wardstudy.domain.reservation.entity.Reservation; 7 | import com.dsg.wardstudy.domain.reservation.dto.NotificationAlarmDto; 8 | import com.dsg.wardstudy.domain.reservation.service.ReservationService; 9 | import com.dsg.wardstudy.domain.user.entity.User; 10 | import com.dsg.wardstudy.repository.reservation.ReservationQueryRepository; 11 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 12 | import com.dsg.wardstudy.repository.user.UserRepository; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.log4j.Log4j2; 15 | import org.springframework.batch.core.Job; 16 | import org.springframework.batch.core.Step; 17 | import org.springframework.batch.core.configuration.annotation.*; 18 | import org.springframework.batch.core.launch.support.RunIdIncrementer; 19 | import org.springframework.batch.item.ItemProcessor; 20 | import org.springframework.batch.item.ItemReader; 21 | import org.springframework.batch.item.ItemWriter; 22 | import org.springframework.batch.item.data.RepositoryItemReader; 23 | import org.springframework.batch.item.data.builder.RepositoryItemReaderBuilder; 24 | import org.springframework.context.annotation.Bean; 25 | import org.springframework.context.annotation.Configuration; 26 | import org.springframework.data.domain.Sort; 27 | 28 | import java.util.Collections; 29 | import java.util.List; 30 | 31 | @Log4j2 32 | @Configuration 33 | @EnableBatchProcessing 34 | @RequiredArgsConstructor 35 | public class NotificationAlarmJobConfig { 36 | private final JobBuilderFactory jobBuilderFactory; 37 | private final StepBuilderFactory stepBuilderFactory; 38 | 39 | private final ReservationService reservationService; 40 | private final ReservationQueryRepository reservationQueryRepository; 41 | private final UserGroupRepository userGroupRepository; 42 | private final UserRepository userRepository; 43 | private final MailSendService mailSendService; 44 | private final MailMessageGenerator messageGenerator; 45 | 46 | private static final int CHUNK_SIZE = 4; 47 | 48 | @Bean("notificationAlarmJob") 49 | public Job notificationAlarmJob(Step notificationAlarmStep) { 50 | return jobBuilderFactory.get("notificationAlarmJob") 51 | .incrementer(new RunIdIncrementer()) 52 | .start(notificationAlarmStep) 53 | .build(); 54 | } 55 | 56 | @JobScope 57 | @Bean("notificationAlarmStep") 58 | public Step notificationAlarmStep(ItemReader notificationAlarmReader, 59 | ItemProcessor notificationAlarmProcessor, 60 | ItemWriter notificationAlarmWriter) { 61 | return stepBuilderFactory.get("notificationAlarmStep") 62 | .chunk(CHUNK_SIZE) 63 | .reader(notificationAlarmReader) 64 | .processor(notificationAlarmProcessor) 65 | .writer(notificationAlarmWriter) 66 | .build(); 67 | } 68 | 69 | @StepScope 70 | @Bean 71 | public RepositoryItemReader notificationAlarmReader() { 72 | return new RepositoryItemReaderBuilder() 73 | .name("notificationAlarmReader") 74 | .repository(userRepository) 75 | .methodName("findBy") 76 | .pageSize(CHUNK_SIZE) 77 | .arguments(List.of()) 78 | .sorts(Collections.singletonMap("id", Sort.Direction.ASC)) 79 | .build(); 80 | } 81 | 82 | @StepScope 83 | @Bean 84 | public ItemProcessor notificationAlarmProcessor() { 85 | return user -> { 86 | 87 | List sgIds = userGroupRepository.findSgIdsByUserId(user.getId()); 88 | log.info("sgIds: {}", sgIds); 89 | 90 | List reservations = 91 | // IsEmailSent.eq(false)인 메일 축출 추가 92 | reservationQueryRepository.findByStartTimeAfterNowAndIsSentFalse(sgIds); 93 | log.info("reservations: {}", reservations); 94 | 95 | return NotificationAlarmDto.builder() 96 | .email(user.getEmail()) 97 | .userName(user.getName()) 98 | .reservations(reservations) 99 | .build(); 100 | }; 101 | } 102 | 103 | @StepScope 104 | @Bean 105 | public ItemWriter notificationAlarmWriter() { 106 | return items -> items.forEach( 107 | item -> { 108 | if(!item.getReservations().isEmpty()){ 109 | String toMessage = messageGenerator.toMessage(item.getUserName(), item.getReservations()); 110 | log.info("sendMail: {}", toMessage); 111 | if(mailSendService.sendMail(item.getEmail(), "ward-study 예약룸 알림", toMessage)) { 112 | for (Reservation r : item.getReservations()) { 113 | reservationService.changeIsEmailSent(r); 114 | } 115 | } 116 | } 117 | } 118 | ); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/dsg/wardstudy/domain/studyGroup/controller/StudyGroupController.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.domain.studyGroup.controller; 2 | 3 | import com.dsg.wardstudy.common.auth.AuthUser; 4 | import com.dsg.wardstudy.domain.studyGroup.dto.PageResponse; 5 | import com.dsg.wardstudy.domain.studyGroup.dto.StudyGroupRequest; 6 | import com.dsg.wardstudy.domain.studyGroup.dto.StudyGroupResponse; 7 | import com.dsg.wardstudy.domain.studyGroup.service.StudyGroupService; 8 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.data.domain.Sort; 13 | import org.springframework.data.web.PageableDefault; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import java.util.List; 19 | 20 | @Log4j2 21 | @RestController 22 | @RequiredArgsConstructor 23 | public class StudyGroupController { 24 | 25 | private final StudyGroupService studyGroupService; 26 | 27 | // 스터디그룹 등록(등록한 유저는 자동 리더가 됨) 28 | @PostMapping("/study-group") 29 | public ResponseEntity register( 30 | @RequestBody StudyGroupRequest studyGroupRequest, 31 | @AuthUser Long userId 32 | ) { 33 | log.info("studyGroup register, studyGroupRequest: {}", studyGroupRequest); 34 | log.info("studyGroup register, userId: {}", userId); 35 | return new ResponseEntity<>(studyGroupService.register(userId, studyGroupRequest), HttpStatus.CREATED); 36 | } 37 | 38 | // 스터디그룹 상세보기 39 | @GetMapping("/study-group/{studyGroupId}") 40 | public ResponseEntity getById( 41 | @PathVariable("studyGroupId") Long studyGroupId, 42 | @AuthUser Long userId 43 | ) { 44 | log.info("studyGroup getById, studyGroupId: {}", studyGroupId); 45 | log.info("studyGroup getById, userId: {}", userId); 46 | return ResponseEntity.ok(studyGroupService.getById(studyGroupId)); 47 | } 48 | 49 | // 스터디그룹 전체조회 paging 조건에 따른 50 | @GetMapping("/study-group/page") 51 | public ResponseEntity getAllPage( 52 | @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable, 53 | @RequestParam(value = "type", required = false) String type, 54 | @RequestParam(value = "keyword", required = false) String keyword 55 | ) { 56 | log.info("studyGroup getAll type: {}, keyword: {}", type, keyword); 57 | return ResponseEntity.ok(studyGroupService.getAll(pageable, type, keyword)); 58 | } 59 | 60 | // 사용자가 참여한 스터디그룹 조회 61 | @GetMapping("/study-group") 62 | public ResponseEntity> getAllByUserId( 63 | @AuthUser Long userId 64 | ) { 65 | log.info("studyGroup getAllByUserId, userId: {}", userId); 66 | return ResponseEntity.ok(studyGroupService.getAllByUserId(userId)); 67 | } 68 | 69 | // 스터디그룹 수정(리더만) 70 | @PutMapping("/study-group/{studyGroupId}") 71 | public Long updateById( 72 | @PathVariable("studyGroupId") Long studyGroupId, 73 | @RequestBody StudyGroupRequest studyGroupRequest, 74 | @AuthUser Long userId 75 | ) { 76 | log.info("studyGroup updateById, userId: {}, studyGroupId: {}, ", userId, studyGroupId); 77 | return studyGroupService.updateById(userId, studyGroupId, studyGroupRequest); 78 | } 79 | 80 | // 스터디그룹 삭제(리더만) 81 | @DeleteMapping("/study-group/{studyGroupId}") 82 | public ResponseEntity deleteById( 83 | @PathVariable("studyGroupId") Long studyGroupId, 84 | @AuthUser Long userId 85 | ) { 86 | log.info("studyGroup deleteById, userId: {}, studyGroupId: {}", userId, studyGroupId); 87 | studyGroupService.deleteById(userId, studyGroupId); 88 | return new ResponseEntity<>("a study-group successfully deleted!", HttpStatus.OK); 89 | } 90 | 91 | // 스터디그룹 참여(일반유저) 92 | @PostMapping("/studyGroup/{studyGroupId}") 93 | public ResponseEntity participate( 94 | @PathVariable Long studyGroupId, 95 | @AuthUser Long userId 96 | ) { 97 | log.info("studyGroup participate, " + 98 | "studyGroupId: {}, userInfo: {}", studyGroupId, userId); 99 | UserGroup participateUG = studyGroupService.participate(userId, studyGroupId); 100 | 101 | return ResponseEntity.ok(participateUG); 102 | } 103 | 104 | // 스터디그룹 좋아요 push 105 | @PostMapping("/studyGroup/{studyGroupId}/likes") 106 | public ResponseEntity like( 107 | @PathVariable Long studyGroupId, 108 | @AuthUser Long userId 109 | ) { 110 | log.info("studyGroup like studyGroupId: {}", studyGroupId); 111 | log.info("studyGroup like userId: {}", userId); 112 | studyGroupService.like(userId, studyGroupId); 113 | 114 | return ResponseEntity.ok("like push success"); 115 | } 116 | 117 | // 스터디그룹 좋아요 카운트 가져오기 118 | @GetMapping("/studyGroup/{studyGroupId}/likes") 119 | public ResponseEntity likeCount( 120 | @PathVariable Long studyGroupId 121 | ) { 122 | log.info("studyGroup likeCount studyGroupId: {}", studyGroupId); 123 | return ResponseEntity.ok(studyGroupService.likeCount(studyGroupId)); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/integration/studyGroup/StudyGroupRepositoryIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.integration.studyGroup; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 4 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 5 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 6 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 7 | import com.dsg.wardstudy.repository.user.UserRepository; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 14 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 15 | 16 | import java.util.List; 17 | import java.util.Optional; 18 | import java.util.stream.Collectors; 19 | import java.util.stream.IntStream; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | @Slf4j 24 | @DataJpaTest 25 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 26 | class StudyGroupRepositoryIntegrationTest { 27 | 28 | @Autowired 29 | private StudyGroupRepository studyGroupRepository; 30 | 31 | @Autowired 32 | private UserGroupRepository userGroupRepository; 33 | 34 | @Autowired 35 | private UserRepository userRepository; 36 | 37 | private StudyGroup studyGroup; 38 | 39 | @BeforeEach 40 | void setup() { 41 | studyGroup = StudyGroup.builder() 42 | .title("testSG") 43 | .content("인원 4명의 스터디그룹을 모집합니다.") 44 | .build(); 45 | } 46 | 47 | @Test 48 | @DisplayName("스터디그룹 생성테스트") 49 | public void givenStudyGroup_whenSave_thenReturnSavedStudyGroup() { 50 | // given - precondition or setup 51 | // when - action or the behaviour that we are going test 52 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 53 | 54 | // then - verify the output 55 | assertThat(savedStudyGroup).isNotNull(); 56 | assertThat(savedStudyGroup.getId()).isGreaterThan(0); 57 | assertThat(savedStudyGroup.getTitle()).isEqualTo("testSG"); 58 | 59 | } 60 | 61 | @Test 62 | @DisplayName("스터디그룹 전체조회") 63 | public void givenStudyGroupList_whenFindAll_thenStudyGroupList() { 64 | // given - precondition or setup 65 | IntStream.rangeClosed(1, 10).forEach(i -> { 66 | StudyGroup studyGroup = StudyGroup.builder() 67 | .title("testSG_" + i) 68 | .content("인원 " + i + "명의 스터디그룹을 모집합니다.") 69 | .build(); 70 | studyGroupRepository.save(studyGroup); 71 | }); 72 | 73 | // when - action or the behaviour that we are going test 74 | List studyGroups = studyGroupRepository.findAll(); 75 | log.info("studyGroups : {}", studyGroups); 76 | 77 | // then - verify the output 78 | assertThat(studyGroups).isNotNull(); 79 | 80 | } 81 | 82 | @Test 83 | @DisplayName("스터디그룹 상세보기") 84 | public void givenStudyGroup_whenFindById_thenReturnStudyGroup() { 85 | // given - precondition or setup 86 | StudyGroup savedStudyGroup = studyGroupRepository.save(this.studyGroup); 87 | // when - action or the behaviour that we are going test 88 | studyGroupRepository.findById(savedStudyGroup.getId()); 89 | 90 | // then - verify the output 91 | assertThat(this.studyGroup).isNotNull(); 92 | 93 | } 94 | 95 | @Test 96 | @DisplayName("스터디그룹들 userId로 가져오기") 97 | public void givenUserId_whenFindByUserId_thenReturnStudyGroupsIdList() { 98 | // given - precondition or setup 99 | // TODO : User, UserGroup save API 만들어야 100 | // User user = User.builder() 101 | // .name("test") 102 | // .email("test@test.com") 103 | // .nickname("test") 104 | // .password("1234") 105 | // .build(); 106 | // User savedUser = userRepository.save(user); 107 | // when - action or the behaviour that we are going test 108 | List iByUserId = userGroupRepository.findByUserId(2L); 109 | List studyGroupsIds = iByUserId.stream() 110 | .map(d -> d.getStudyGroup().getId()) 111 | .collect(Collectors.toList()); 112 | 113 | List studyGroups = studyGroupRepository.findByIdIn(studyGroupsIds); 114 | // then - verify the output 115 | assertThat(studyGroups).isNotNull(); 116 | log.info("studyGroups.size(): " + studyGroups.size()); 117 | 118 | 119 | } 120 | 121 | @Test 122 | @DisplayName("스터디그룹 수정") 123 | public void givenStudyGroup_whenUpdateStudyGroup_thenReturnUpdatedStudyGroup() { 124 | // given - precondition or setup 125 | studyGroupRepository.save(studyGroup); 126 | 127 | // when - action or the behaviour that we are going test 128 | StudyGroup savedStudyGroup = studyGroupRepository.findById(this.studyGroup.getId()).get(); 129 | savedStudyGroup.update("new_title", "new_content"); 130 | 131 | // then - verify the output 132 | assertThat(studyGroup.getTitle()).isEqualTo("new_title"); 133 | 134 | } 135 | 136 | @Test 137 | @DisplayName("스터디그룹 삭제") 138 | public void givenStudyGroup_whenDelete_thenRemoveStudyGroup() { 139 | // given - precondition or setup 140 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 141 | // when - action or the behaviour that we are going test 142 | studyGroupRepository.deleteById(savedStudyGroup.getId()); 143 | Optional deletedStudyGroup = studyGroupRepository.findById(savedStudyGroup.getId()); 144 | 145 | // then - verify the output 146 | assertThat(deletedStudyGroup).isEmpty(); 147 | 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/repository/studyGroup/StudyGroupRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.repository.studyGroup; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 4 | import com.dsg.wardstudy.domain.user.entity.User; 5 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 6 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 7 | import com.dsg.wardstudy.repository.user.UserRepository; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | import java.util.stream.Collectors; 18 | import java.util.stream.IntStream; 19 | import java.util.stream.LongStream; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | @Slf4j 24 | @DataJpaTest 25 | class StudyGroupRepositoryTest { 26 | 27 | @Autowired 28 | private StudyGroupRepository studyGroupRepository; 29 | 30 | @Autowired 31 | private UserRepository userRepository; 32 | @Autowired 33 | private UserGroupRepository userGroupRepository; 34 | 35 | private StudyGroup studyGroup; 36 | private User user; 37 | private UserGroup userGroup; 38 | 39 | 40 | @BeforeEach 41 | void setup() { 42 | studyGroup = StudyGroup.builder() 43 | .id(1L) 44 | .title("testSG") 45 | .content("인원 4명의 스터디그룹을 모집합니다.") 46 | .build(); 47 | 48 | user = User.builder() 49 | .id(1L) 50 | .name("dsg") 51 | .email("dsg@gmail.com") 52 | .build(); 53 | 54 | userGroup = UserGroup.builder() 55 | .user(user) 56 | .studyGroup(studyGroup) 57 | .build(); 58 | } 59 | 60 | @Test 61 | @DisplayName("스터디그룹 생성테스트") 62 | public void givenStudyGroup_whenSave_thenReturnSavedStudyGroup() { 63 | // given - precondition or setup 64 | // when - action or the behaviour that we are going test 65 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 66 | 67 | // then - verify the output 68 | assertThat(savedStudyGroup).isNotNull(); 69 | assertThat(savedStudyGroup.getTitle()).isEqualTo("testSG"); 70 | 71 | } 72 | 73 | @Test 74 | @DisplayName("스터디그룹 전체조회") 75 | public void givenStudyGroupList_whenFindAll_thenStudyGroupList() { 76 | // given - precondition or setup 77 | // when - action or the behaviour that we are going test 78 | IntStream.rangeClosed(1, 10).forEach(i -> { 79 | StudyGroup studyGroup = StudyGroup.builder() 80 | .title("testSG_" + i) 81 | .content("인원 " + i + "명의 스터디그룹을 모집합니다.") 82 | .build(); 83 | studyGroupRepository.save(studyGroup); 84 | }); 85 | 86 | 87 | List studyGroups = studyGroupRepository.findAll(); 88 | log.info("studyGroups : {}", studyGroups); 89 | 90 | // then - verify the output 91 | assertThat(studyGroups).isNotNull(); 92 | assertThat(studyGroups.size()).isEqualTo(10); 93 | 94 | } 95 | 96 | @Test 97 | @DisplayName("스터디그룹 상세보기") 98 | public void givenStudyGroup_whenFindById_thenReturnStudyGroup() { 99 | // given - precondition or setup 100 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 101 | // when - action or the behaviour that we are going test 102 | StudyGroup findStudyGroup = studyGroupRepository.findById(savedStudyGroup.getId()).get(); 103 | 104 | // then - verify the output 105 | assertThat(this.studyGroup).isNotNull(); 106 | assertThat(findStudyGroup.getTitle()).isEqualTo("testSG"); 107 | 108 | } 109 | 110 | @Test 111 | @DisplayName("스터디그룹들 userId로 가져오기") 112 | public void givenUserId_whenFindByUserId_thenReturnStudyGroupsIdList() { 113 | // given - precondition or setup 114 | userRepository.save(user); 115 | studyGroupRepository.save(studyGroup); 116 | userGroupRepository.save(userGroup); 117 | 118 | // when - action or the behaviour that we are going test 119 | List userGroupList = userGroupRepository.findByUserId(user.getId()); 120 | log.info("userGroupList: {}", userGroupList); 121 | List studyGroupsIds = userGroupList.stream() 122 | .map(ug -> ug.getStudyGroup().getId()) 123 | .collect(Collectors.toList()); 124 | log.info("studyGroupsIds : {}", studyGroupsIds); 125 | 126 | assertThat(studyGroupsIds).isNotNull(); 127 | assertThat(studyGroupsIds).isEqualTo(List.of(studyGroup.getId())); 128 | 129 | } 130 | 131 | // TODO : save가 하나만 되는 현상 132 | @Test 133 | @DisplayName("스터디그룹들 studyGroupIdList로 가져오기") 134 | public void givenSGIdList_whenFindBySGIdIn_thenStudyGroupList(){ 135 | // given - precondition or setup 136 | LongStream.rangeClosed(1, 3).forEach(i -> { 137 | StudyGroup studyGroup = StudyGroup.builder() 138 | .id(i) 139 | .title("testSG_" + i) 140 | .content("인원 " + i + "명의 스터디그룹을 모집합니다.") 141 | .build(); 142 | studyGroupRepository.save(studyGroup); 143 | }); 144 | 145 | List studyGroupsIds = List.of(2L, 3L); 146 | 147 | // when - action or the behaviour that we are going test 148 | List studyGroups = studyGroupRepository.findByIdIn(studyGroupsIds); 149 | log.info("studyGroups: {}", studyGroups); 150 | // then - verify the output 151 | assertThat(studyGroups).isNotNull(); 152 | log.info("studyGroups.size(): " + studyGroups.size()); 153 | assertThat(studyGroups.size()).isEqualTo(3); 154 | 155 | } 156 | 157 | @Test 158 | @DisplayName("스터디그룹 수정") 159 | public void givenStudyGroup_whenUpdateStudyGroup_thenReturnUpdatedStudyGroup() { 160 | // given - precondition or setup 161 | // when - action or the behaviour that we are going test 162 | StudyGroup savedStudyGroup = studyGroupRepository.save(this.studyGroup); 163 | 164 | log.info("savedStudyGroup: {}", savedStudyGroup); 165 | savedStudyGroup.update("new_title", "new_content"); 166 | 167 | // then - verify the output 168 | assertThat(savedStudyGroup).isNotNull(); 169 | assertThat(savedStudyGroup.getTitle()).isEqualTo("new_title"); 170 | assertThat(savedStudyGroup.getContent()).isEqualTo("new_content"); 171 | 172 | } 173 | 174 | @Test 175 | @DisplayName("스터디그룹 삭제") 176 | public void givenStudyGroup_whenDelete_thenRemoveStudyGroup() { 177 | // given - precondition or setup 178 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 179 | // when - action or the behaviour that we are going test 180 | studyGroupRepository.delete(savedStudyGroup); 181 | Optional deletedStudyGroup = studyGroupRepository.findById(1L); 182 | 183 | // then - verify the output 184 | assertThat(deletedStudyGroup).isEmpty(); 185 | 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/test/java/com/dsg/wardstudy/integration/studyGroup/StudyGroupControllerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.dsg.wardstudy.integration.studyGroup; 2 | 3 | import com.dsg.wardstudy.domain.studyGroup.entity.StudyGroup; 4 | import com.dsg.wardstudy.domain.user.entity.User; 5 | import com.dsg.wardstudy.domain.user.entity.UserGroup; 6 | import com.dsg.wardstudy.domain.studyGroup.dto.StudyGroupRequest; 7 | import com.dsg.wardstudy.repository.studyGroup.StudyGroupRepository; 8 | import com.dsg.wardstudy.repository.user.UserGroupRepository; 9 | import com.dsg.wardstudy.repository.user.UserRepository; 10 | import com.dsg.wardstudy.domain.user.constant.UserType; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 17 | import org.springframework.boot.test.context.SpringBootTest; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.test.web.servlet.ResultActions; 21 | 22 | import java.util.stream.IntStream; 23 | 24 | import static org.hamcrest.CoreMatchers.is; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 29 | 30 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 31 | @AutoConfigureMockMvc 32 | class StudyGroupControllerIntegrationTest { 33 | 34 | @Autowired 35 | private MockMvc mockMvc; 36 | 37 | @Autowired 38 | private StudyGroupRepository studyGroupRepository; 39 | 40 | @Autowired 41 | private UserRepository userRepository; 42 | 43 | @Autowired 44 | private UserGroupRepository userGroupRepository; 45 | 46 | @Autowired 47 | private ObjectMapper objectMapper; 48 | 49 | private StudyGroup studyGroup; 50 | 51 | private User user; 52 | private UserGroup userGroup; 53 | 54 | @BeforeEach 55 | void setup() { 56 | objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); 57 | 58 | studyGroup = StudyGroup.builder() 59 | .title("testSG") 60 | .content("인원 4명의 스터디그룹을 모집합니다.") 61 | .build(); 62 | 63 | user = User.builder() 64 | .id(1L) 65 | .build(); 66 | 67 | userGroup = UserGroup.builder() 68 | .user(user) 69 | .studyGroup(studyGroup) 70 | .userType(UserType.LEADER) 71 | .build(); 72 | } 73 | 74 | @Test 75 | public void givenStudyGroupRequest_whenCreate_thenReturnStudyGroupResponse() throws Exception { 76 | // given - precondition or setup 77 | StudyGroupRequest studyGroupRequest = StudyGroupRequest.builder() 78 | .title(studyGroup.getTitle()) 79 | .content(studyGroup.getContent()) 80 | .build(); 81 | 82 | // when - action or the behaviour that we are going test 83 | ResultActions resultActions = mockMvc.perform(post("/study-group") 84 | .contentType(MediaType.APPLICATION_JSON) 85 | .content(objectMapper.writeValueAsString(studyGroupRequest))); 86 | 87 | // then - verify the output 88 | resultActions 89 | .andDo(print()) 90 | .andExpect(status().isCreated()) 91 | .andExpect(jsonPath("$.title", is(studyGroup.getTitle()))) 92 | .andExpect(jsonPath("$.content", is(studyGroup.getContent()))); 93 | 94 | } 95 | 96 | @Test 97 | public void givenListOfStudyGroupResponses_whenGet_thenReturnStudyGroupResponseList() throws Exception { 98 | // given - precondition or setup 99 | int length = 3; 100 | IntStream.rangeClosed(1, length).forEach(i -> { 101 | StudyGroup studyGroup = StudyGroup.builder() 102 | .title("sg_dsg" + "_" + i) 103 | .content("spring_study" + "_" + i) 104 | .build(); 105 | studyGroupRepository.save(studyGroup); 106 | }); 107 | 108 | // when - action or the behaviour that we are going test 109 | // then - verify the output 110 | mockMvc.perform(get("/study-group") 111 | .param("page", "0") 112 | .param("size", "10") 113 | .param("sort", "id,desc") 114 | ) 115 | .andDo(print()) 116 | .andExpect(status().isOk()); 117 | 118 | 119 | } 120 | 121 | @Test 122 | public void givenStudyGroupId_whenGet_thenReturnStudyGroupResponse() throws Exception { 123 | // given - precondition or setup 124 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 125 | 126 | // when - action or the behaviour that we are going test 127 | // then - verify the output 128 | mockMvc.perform(get("/study-group/" + savedStudyGroup.getId())) 129 | .andDo(print()) 130 | .andExpect(status().isOk()) 131 | .andExpect(jsonPath("$.title", is(studyGroup.getTitle()))) 132 | .andExpect(jsonPath("$.content", is(studyGroup.getContent()))); 133 | 134 | } 135 | 136 | @Test 137 | public void givenInvalidStudyGroupId_whenGet_thenReturnEmpty() throws Exception { 138 | Long studyGroupId = 100L; 139 | // given - precondition or setup 140 | studyGroupRepository.save(studyGroup); 141 | 142 | // when - action or the behaviour that we are going test 143 | // then - verify the output 144 | mockMvc.perform(get("/study-group/" + studyGroupId)) 145 | .andDo(print()) 146 | .andExpect(status().isNotFound()); 147 | 148 | } 149 | 150 | @Test 151 | public void givenUserId_whenGetAll_thenReturnStudyGroupResponseList() throws Exception { 152 | // given - precondition or setup 153 | Long userId = 1L; 154 | 155 | studyGroupRepository.save(studyGroup); 156 | User savedUser = userRepository.save(user); 157 | userGroupRepository.save(userGroup); 158 | 159 | 160 | // when - action or the behaviour that we are going test 161 | // then - verify the output 162 | mockMvc.perform(get("/users/{userId}/study-group",savedUser.getId()) 163 | .contentType(MediaType.APPLICATION_JSON)) 164 | .andDo(print()) 165 | .andExpect(status().isOk()); 166 | 167 | } 168 | 169 | @Test 170 | public void givenStudyGroupIdAndUpdatedStudyGroupRequest_whenUpdate_thenReturnUpdateStudyGroupId() throws Exception { 171 | // given - precondition or setup 172 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 173 | 174 | StudyGroupRequest updateStudyGroupRequest = StudyGroupRequest.builder() 175 | .title("Jasi") 176 | .content("springboot_study!!") 177 | .build(); 178 | 179 | 180 | // when - action or the behaviour that we are going test 181 | // then - verify the output 182 | mockMvc.perform(put("/study-group/{id}", savedStudyGroup.getId()) 183 | .contentType(MediaType.APPLICATION_JSON) 184 | .content(objectMapper.writeValueAsString(updateStudyGroupRequest))) 185 | .andDo(print()) 186 | .andExpect(status().isOk()); 187 | 188 | } 189 | 190 | @Test 191 | public void givenStudyGroupId_whenDelete_thenReturn200() throws Exception { 192 | // given - precondition or setup 193 | StudyGroup savedStudyGroup = studyGroupRepository.save(studyGroup); 194 | 195 | // when - action or the behaviour that we are going test 196 | // then - verify the output 197 | mockMvc.perform(delete("/study-group/{id}", savedStudyGroup.getId())) 198 | .andDo(print()) 199 | .andExpect(status().isOk()); 200 | 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | --------------------------------------------------------------------------------