diff --git a/.github/ISSUE_TEMPLATE/bug-fix-template.md b/.github/ISSUE_TEMPLATE/bug-fix-template.md
new file mode 100644
index 0000000..3e5f17f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-fix-template.md
@@ -0,0 +1,17 @@
+---
+name: "\bBug Fix Template"
+about: 버그 이슈 템플릿
+title: "[BUG] "
+labels: bug
+assignees: ''
+
+---
+
+## 🐞 어떤 상황에서 발생한 버그인가요?
+> 어떤 상황에서 발생한 버그인지 설명해주세요
+
+## 🎁 수정한 내용
+> 어떤 작업으로 버그를 잡았는지 설명해주세요
+
+
+## 🔍 참고할만한 자료(선택)
diff --git a/.github/ISSUE_TEMPLATE/feature-template.md b/.github/ISSUE_TEMPLATE/feature-template.md
new file mode 100644
index 0000000..3ff4034
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-template.md
@@ -0,0 +1,20 @@
+---
+name: Feature Template
+about: 기능 추가 이슈 템플릿
+title: "[FEAT] "
+labels: feature
+assignees: ''
+
+---
+
+## 📌 어떤 기능인가요?
+> 추가하려는 기능에 대해 간결하게 설명해주세요
+
+
+## 📜 작업 상세 내용
+- [ ] TODO
+- [ ] TODO
+- [ ] TODO
+
+
+## 🔍 참고할만한 자료(선택)
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/refactor-issue-template.md b/.github/ISSUE_TEMPLATE/refactor-issue-template.md
new file mode 100644
index 0000000..7d3d8ee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/refactor-issue-template.md
@@ -0,0 +1,19 @@
+---
+name: Refactor issue template
+about: 리팩터링 이슈 템플릿
+title: "[REFACTOR] "
+labels: refactor
+assignees: ''
+
+---
+
+## 📌 어떤 기능을 리팩터링 하나요?
+> 리팩터링 할 기능에 대해 간결하게 설명해주세요
+
+## AS-IS
+> 현재 인식한 상황에 대해 설명해주세요
+
+## TO-BE
+> 현재의 상황에서 개선시킬 이상적인 지향점을 설명해주세요
+
+## 🔍 참고할만한 자료(선택)
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/yummy-issue-template.md b/.github/ISSUE_TEMPLATE/yummy-issue-template.md
index 0fc6e3a..4356d18 100644
--- a/.github/ISSUE_TEMPLATE/yummy-issue-template.md
+++ b/.github/ISSUE_TEMPLATE/yummy-issue-template.md
@@ -8,11 +8,9 @@ assignees: ''
---
## 📋 이슈 내용
-
> 추가하려는 기능에 대해 간결하게 설명해주세요
## ✅ 체크리스트
-
- [ ] TODO
- [ ] TODO
- [ ] TODO
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 76feb13..836b893 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,10 @@
-## 📋 이슈 번호
+## ⚡️ 관련 이슈
+- close #이슈번호
-## 🛠 구현 사항
+## 📍주요 변경 사항
+> 주요 변경 사항에 대해 작성해주세요.
+
+
+## 🎸기타
+> 고려해야 하는 내용을 작성해 주세요.
-## 📚 기타
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..ee509c8
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,64 @@
+name: Build and Deploy to EC2
+
+on:
+ push:
+ branches:
+ - main
+ - dev
+
+jobs:
+ build-and-push-docker:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Cache Gradle packages
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build with Gradle
+ run: ./gradlew clean build
+
+# - name: application.properties 파일 생성
+# run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./application/src/main/resources/application.properties
+# cat ./application/src/main/resources/application.properties
+
+ - name: AWS Resource에 접근할 수 있게 AWS credentials 설정
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-region: ap-northeast-2
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID}}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
+
+ - name: ECR login
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v2
+
+ - name: Docker 이미지 생성
+ run: docker build -t my-yummy .
+
+ - name: Docker 이미지에 태그 붙이기
+ run: docker tag my-yummy ${{ secrets.ECR_REGISTRY }}/my-yummy:latest
+
+ - name: ECR에 Docker 이미지 Push
+ run: docker push ${{ secrets.ECR_REGISTRY }}/my-yummy:latest
+
+ - name: SSH로 EC2에 접속하기
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.EC2_HOST}}
+ username: ${{ secrets.EC2_USERNAME}}
+ key: ${{ secrets.EC2_PRIVATE_KEY}}
+ script_stop: true
+ script: |
+ cd ~/my-yummy
+ docker compose down || true
+ docker pull ${{ secrets.ECR_REGISTRY }}/my-yummy:latest
+ docker compose up -d --build
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..edcdf6a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: BE CI
+
+on:
+ pull_request:
+ branches:
+ - main
+ - dev
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Github Repository 파일 불러오기
+ uses: actions/checkout@v4
+
+ - name: JDK 17 설정
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 17
+
+ - name: 테스트 코드 실행
+ run: ./gradlew test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c2065bc..7a0f4d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,7 @@ bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
-.idea
+.idea/
*.iws
*.iml
*.ipr
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ff092ea
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,3 @@
+FROM openjdk:17-jdk
+COPY application/build/libs/yummy_server.jar yummy_server.jar
+ENTRYPOINT ["java", "-jar", "yummy_server.jar"]
diff --git a/README.md b/README.md
index 38198af..56c6d1a 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,23 @@
-# famous_restaurant
+# Yummy
+> 지역 맛집 탐방 소모임 형성 서비스
-Re:quire's famous restaurant service rep
+# 🚀 기능
+## 핵심 기능
+### 카카오 소셜 로그인
+### 실제 가게 정보 연동 API 구현
+### 소모임 생성 및 참가
+### 소모임 댓글 등록
----
+## API 명세
+
-# 커밋 컨벤션
+# ⿳ ERD
+
- - 예시
- - feat: hyungjun#12-feat-create-user-api - #2
+# 🏛️ 멀티 모듈 설계 및 흐름도
+
+
-- feat: 새로운 기능 추가
-- fix: 버그 수정
-- docs: 문서 수정
-- style: 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
-- refactor: 코드 리팩토링
-- test: 테스트 코드, 리팩토링 테스트 코드 추가
-- chore: 빌드 업무 수정, 패키지 매니저 수정
+# 🗺️ 아키텍쳐
+
-# 이슈 템플릿
-
- - issue 제목
- - 예시: feat: 이슈 정리
- - issue 템플릿
-
- ```markdown
- ## 📋 이슈 내용
-
- ## ✅ 체크리스트
-
- ## 📚 레퍼런스
-
- ```
- - 제목 예시
- - add: UI button 구현
-
-# branch 규칙
-
- - 각자 영어이름#이슈번호-이슈타입-이슈제목
- - 예시: hyungjun#12-feat-create-user-api
-
- - 종류: 메시지 - #이슈번호
- - 예시
- - feat: hyungjun#12-feat-create-user-api - #2
-
-# PR 템플릿
-
- - PR 템플릿
-
- ```markdown
- ## 📋 이슈 번호
-
- ## 🛠 구현 사항
-
- ## 📚 기타
-
- ```
-
-# merge 컨벤션
-
- - merge: 브랜치 이름 - #Issue 번호 혹은 PR 번호
- - 예시
- - merge: main <-hyungjun#1-feat-user-controller
-
-# ERD
-
----
-
-
-
-# 🗺️아키텍처
-
----
-
-# 💡기능
-
----
diff --git a/application/build.gradle b/application/build.gradle
index 6eb68c3..7c5451b 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -1,7 +1,7 @@
plugins {
id 'java'
- id 'org.springframework.boot' version '3.4.1'
- id 'io.spring.dependency-management' version '1.1.7'
+ id 'org.springframework.boot'
+ id 'io.spring.dependency-management'
}
group = 'com.groom'
@@ -14,15 +14,53 @@ java {
}
dependencies {
+ // 모듈 간 의존성
+ implementation project(':storage')
+ implementation project(':domain')
+ implementation project(':common')
+
+ // Spring Boot Starters
+ implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+
+ // OAuth2
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+
+ // JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+ // Swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
+
+ // Database
+ runtimeOnly 'com.h2database:h2'
+
+ // Lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+
+ // Testing Dependencies
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ testImplementation 'javax.servlet:javax.servlet-api:4.0.1'
- implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.springframework.boot:spring-boot-starter'
+ // Mocking and JSON Processing
+ testImplementation 'com.squareup.okhttp3:mockwebserver:4.11.0'
+ testImplementation 'org.mockito:mockito-core:5.6.0'
+ testImplementation 'org.mockito:mockito-junit-jupiter:5.7.0'
+ testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation project(':domain')
+ // Netty
+ implementation "io.netty:netty-resolver-dns-native-macos:4.1.110.Final:osx-x86_64"
}
test {
@@ -30,6 +68,8 @@ test {
}
bootJar {
+ mainClass = 'com.groom.yummy.YummyApplication'
+ archiveFileName = "yummy_server.jar"
enabled = true
}
diff --git a/application/src/main/java/com/groom/yummy/YummyApplication.java b/application/src/main/java/com/groom/yummy/YummyApplication.java
new file mode 100644
index 0000000..6ed48a4
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/YummyApplication.java
@@ -0,0 +1,9 @@
+package com.groom.yummy;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+ public class YummyApplication {
+ public static void main(String[] args){ SpringApplication.run(YummyApplication.class, args); }
+}
diff --git a/application/src/main/java/com/groom/yummy/config/CorsMvcConfig.java b/application/src/main/java/com/groom/yummy/config/CorsMvcConfig.java
new file mode 100644
index 0000000..f4240ea
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/config/CorsMvcConfig.java
@@ -0,0 +1,20 @@
+package com.groom.yummy.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsMvcConfig implements WebMvcConfigurer {
+
+ @Value("${server.url}")
+ private String SERVER_URL;
+
+ @Override
+ public void addCorsMappings(CorsRegistry corsRegistry){
+ corsRegistry.addMapping("/**")
+ .exposedHeaders("Set-Cookie")
+ .allowedOrigins(SERVER_URL);
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/config/SecurityConfig.java b/application/src/main/java/com/groom/yummy/config/SecurityConfig.java
new file mode 100644
index 0000000..5e14c14
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/config/SecurityConfig.java
@@ -0,0 +1,91 @@
+package com.groom.yummy.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.exception.security.CustomAccessDeniedHandler;
+import com.groom.yummy.exception.security.CustomAuthenticationEntryPoint;
+import com.groom.yummy.filter.JwtAuthFilter;
+import com.groom.yummy.oauth2.handler.CustomSuccessHandler;
+import com.groom.yummy.oauth2.service.CustomOAuth2UserService;
+import com.groom.yummy.jwt.JwtProvider;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final CustomOAuth2UserService customOAuth2UserService;
+ private final CustomSuccessHandler customSuccessHandler;
+ private final JwtProvider jwtUtil;
+ private final ObjectMapper objectMapper;
+
+ @Value("${server.url}")
+ private String SERVER_URL;
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
+ //cors 설정
+ http.cors((cors -> cors.configurationSource(configurationSource())));
+
+ // csfr disable
+ http.csrf((auth) -> auth.disable());
+
+ // form 로그인 disable
+ http.formLogin((auth) -> auth.disable());
+
+ // HTTP Basic 인증 방식 disable
+ http.httpBasic((auth) -> auth.disable());
+
+
+ //경로별 인가 작업
+ http.authorizeHttpRequests((auth) -> auth
+ .requestMatchers( "/login/**", "/oauth2/**", "/oauth2/authorization/**").permitAll()
+ .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() // Swagger 관련 경로 허용
+ .requestMatchers("/api/v1/test/users/*").permitAll()
+ .requestMatchers("/api/v1/stores/*","/api/v1/stores").permitAll()
+ .anyRequest().authenticated());
+
+ //세션 설정 : STATELESS
+ http.sessionManagement((session) -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+ //oauth2
+ http.oauth2Login((oauth2) ->
+ oauth2.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
+ .userService(customOAuth2UserService)).successHandler(customSuccessHandler));
+
+ // JWTFilter 추가
+ http.addFilterBefore(new JwtAuthFilter(jwtUtil,objectMapper), UsernamePasswordAuthenticationFilter.class);
+
+ // Exception handler 추가
+ http.exceptionHandling(exceptionHandling ->
+ exceptionHandling
+ .accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper))
+ .authenticationEntryPoint(new CustomAuthenticationEntryPoint(objectMapper)));
+ return http.build();
+ }
+
+ public CorsConfigurationSource configurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.addAllowedHeader("*");
+ configuration.addAllowedMethod("*");
+ configuration.addAllowedOrigin(SERVER_URL); // 특정 도메인 허용
+ configuration.addAllowedOrigin("http://13.124.191.4:8080"); // 특정 도메인 허용
+ configuration.setAllowCredentials(true);
+ configuration.addExposedHeader("ACCESS_TOKEN");
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration); // 모든 주소요청에 위 설정을 넣어주겠다.
+ return source;
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/config/SwaggerConfig.java b/application/src/main/java/com/groom/yummy/config/SwaggerConfig.java
new file mode 100644
index 0000000..36dfeb8
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/config/SwaggerConfig.java
@@ -0,0 +1,40 @@
+package com.groom.yummy.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.servers.Server;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+public class SwaggerConfig {
+
+ @Value("${server.url}")
+ private String SERVER_URL;
+
+ @Bean
+ public OpenAPI openAPI() {
+ return new OpenAPI()
+ .components(new Components())
+ .info(apiInfo())
+ .servers(servers());
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("yummy 명세서")
+ .description("api 명세서")
+ .version("1.0.0");
+ }
+
+ private List servers() {
+ List servers = new ArrayList<>();
+ servers.add(new Server().url(SERVER_URL).description("YUMMY 명세서"));
+ return servers;
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/config/WebClientConfig.java b/application/src/main/java/com/groom/yummy/config/WebClientConfig.java
new file mode 100755
index 0000000..73046c4
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/config/WebClientConfig.java
@@ -0,0 +1,14 @@
+package com.groom.yummy.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Configuration
+public class WebClientConfig {
+ @Bean
+ public WebClient webClient() {
+ return WebClient.builder().baseUrl("http://43.203.242.150:8080").build(); // 실제 환경에서는 동적으로 설정
+ }
+}
+
diff --git a/application/src/main/java/com/groom/yummy/controller/GroupController.java b/application/src/main/java/com/groom/yummy/controller/GroupController.java
new file mode 100755
index 0000000..275c2ca
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/controller/GroupController.java
@@ -0,0 +1,95 @@
+package com.groom.yummy.controller;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.group.GroupService;
+import com.groom.yummy.group.dto.request.CreateGroupRequestDto;
+import com.groom.yummy.group.dto.request.JoinGroupRequestDto;
+import com.groom.yummy.group.dto.response.GroupDetailResponseDto;
+import com.groom.yummy.group.dto.response.GroupResponseDto;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Tag(name = "[Group] Group API")
+@RestController
+@RequestMapping("/api/v1/groups")
+@RequiredArgsConstructor
+public class GroupController {
+
+ private final GroupService groupService;
+
+ @Operation(summary = "소모임 생성", description = "소모임을 생성합니다.")
+ @PostMapping
+ public ResponseEntity> createGroup(
+ @Valid @RequestBody CreateGroupRequestDto requestDto,
+ @AuthenticationPrincipal LoginUser loginUser
+ ) {
+ Long userId = loginUser.getUserId();
+ Long groupId = groupService.createGroup(
+ requestDto.getStoreId(),
+ userId,
+ requestDto.getTitle(),
+ requestDto.getContent(),
+ requestDto.getMaxParticipants(),
+ requestDto.getMinParticipants(),
+ requestDto.getMeetingDate()
+ );
+
+ GroupResponseDto response = groupService.findGroupById(groupId)
+ .map(GroupResponseDto::fromGroupDomain)
+ .orElseThrow();
+
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(new ResponseDto<>(response, "소모임 생성 성공"));
+ }
+
+ @Operation(summary = "소모임 리스트 조회", description = "생성되어있는 소모임 리스트를 출력합니다.")
+ @GetMapping
+ public ResponseEntity>> getAllGroups(
+ @RequestParam(required = false) String category,
+ @RequestParam(required = false) String regionCode,
+ @RequestParam(required = false) String storeName,
+ @RequestParam(defaultValue = "1") int page
+ ) {
+ List groups = groupService.getAllGroups(category, regionCode, storeName, page)
+ .stream()
+ .map(GroupResponseDto::fromGroupDomain)
+ .collect(Collectors.toList());
+
+ return ResponseEntity.ok(new ResponseDto<>(groups, "소모임 목록 조회 성공"));
+ }
+
+ @Operation(summary = "소모임 상세 정보 조회", description = "소모임 ID 기반으로 소모임의 상세 정보를 출력합니다.")
+ @GetMapping("/{groupId}")
+ public ResponseEntity> getGroupById(
+ @PathVariable Long groupId
+ ) {
+ GroupDetailResponseDto response = groupService.findGroupById(groupId)
+ .map(GroupDetailResponseDto::fromGroupDomain)
+ .orElseThrow(() -> new IllegalArgumentException("소모임을 찾을 수 없습니다."));
+
+ return ResponseEntity.ok(new ResponseDto<>(response, "소모임 상세 정보 조회 성공"));
+ }
+
+ @Operation(summary = "소모임 참가", description = "선택한 소모임에 참가합니다.")
+ @PostMapping("/{groupId}/join")
+ public ResponseEntity> joinGroup(
+ @PathVariable Long groupId,
+ @Valid @RequestBody JoinGroupRequestDto requestDto,
+ @AuthenticationPrincipal LoginUser loginUser
+ ) {
+ Long userId = loginUser.getUserId();
+ groupService.joinGroup(groupId, userId, requestDto.getStoreId());
+ return ResponseEntity.ok(new ResponseDto<>(null, "소모임 참여 성공"));
+ }
+}
+
diff --git a/application/src/main/java/com/groom/yummy/controller/ReplyController.java b/application/src/main/java/com/groom/yummy/controller/ReplyController.java
new file mode 100644
index 0000000..69923e5
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/controller/ReplyController.java
@@ -0,0 +1,175 @@
+package com.groom.yummy.controller;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.reply.Reply;
+import com.groom.yummy.reply.ReplyService;
+import com.groom.yummy.reply.dto.ReplyRequestDto;
+import com.groom.yummy.reply.dto.ReplyResponseDto;
+import com.groom.yummy.reply.dto.ReplyUpdateRequestDto;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+
+@Tag(name = "[Reply] Reply API")
+@RestController
+@RequestMapping("/api/v1/reply")
+@RequiredArgsConstructor
+public class ReplyController {
+
+ private final ReplyService replyService;
+
+ @Operation(
+ summary = "댓글 등록",
+ description = "댓글을 등록합니다.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "댓글 작성 성공",
+ content = @Content(schema = @Schema(implementation = ReplyResponseDto.class))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청 데이터"
+ )
+ }
+ )
+ @PostMapping
+ public ResponseEntity> registerReply(@Valid @RequestBody ReplyRequestDto request) {
+ try {
+ Reply savedReply = replyService.createReply(request.toDomain());
+ ReplyResponseDto responseDto = ReplyResponseDto.fromDomain(savedReply);
+ return ResponseEntity.ok(new ResponseDto<>(responseDto, "댓글을 성공적으로 작성하였습니다."));
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(new ResponseDto<>(-1, "댓글 작성 실패: " + e.getMessage()));
+ }
+ }
+
+ @Operation(
+ summary = "댓글 목록 조회",
+ description = "특정 그룹 ID에 속한 모든 댓글을 페이지 단위로 조회합니다.",
+ parameters = {
+ @Parameter(name = "groupId", description = "그룹 ID", required = true),
+ @Parameter(name = "page", description = "조회할 페이지 번호 (0부터 시작)", example = "0"),
+ @Parameter(name = "size", description = "페이지 크기", example = "10")
+ },
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "댓글 조회 성공",
+ content = @Content(schema = @Schema(implementation = ReplyResponseDto.class))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청 데이터"
+ )
+ }
+ )
+ @GetMapping(path = "/all")
+ public ResponseEntity> getAllReplies(
+ @RequestParam Long groupId,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "10") int size) {
+ try {
+ PageRequest pageRequest = PageRequest.of(page, size);
+ Page replies = replyService.getAllReplies(groupId, pageRequest);
+ Page responseDtos = replies.map(ReplyResponseDto::fromDomain);
+ return ResponseEntity.ok(new ResponseDto<>(responseDtos, "댓글 조회 성공"));
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(new ResponseDto<>(-1, "댓글 조회 실패: " + e.getMessage()));
+ }
+ }
+
+ @Operation(
+ summary = "댓글 수정",
+ description = "댓글의 내용을 수정합니다.",
+ parameters = {
+ @Parameter(name = "id", description = "수정할 댓글 ID", required = true)
+ },
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "댓글 수정 성공",
+ content = @Content(schema = @Schema(implementation = ReplyResponseDto.class))
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청 데이터 또는 댓글이 존재하지 않음"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 오류"
+ )
+ }
+ )
+ @PatchMapping("/{id}")
+ public ResponseEntity> updateReply(@PathVariable Long id, @Valid @RequestBody ReplyUpdateRequestDto request) {
+ try {
+ Reply updatedReply = replyService.updateReply(id, request.content());
+ ReplyResponseDto responseDto = ReplyResponseDto.fromDomain(updatedReply);
+ return ResponseEntity.ok(new ResponseDto<>(responseDto, "댓글이 성공적으로 수정되었습니다."));
+ } catch (IllegalArgumentException e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(new ResponseDto<>(-1, "댓글 수정 실패: " + e.getMessage()));
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new ResponseDto<>(-1, "서버 오류: " + e.getMessage()));
+ }
+ }
+
+ @Operation(
+ summary = "댓글 삭제",
+ description = "댓글을 삭제합니다.",
+ parameters = {
+ @Parameter(name = "id", description = "삭제할 댓글 ID", required = true)
+ },
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "댓글 삭제 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청 데이터 또는 댓글 삭제 실패"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 오류"
+ )
+ }
+ )
+ @DeleteMapping("/{id}")
+ public ResponseEntity> deleteReply(@PathVariable Long id) {
+ try {
+ replyService.deleteReply(id);
+ return ResponseEntity.ok(new ResponseDto<>(null, "댓글이 성공적으로 삭제되었습니다."));
+ } catch (IllegalStateException e) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .body(new ResponseDto<>(-1, "댓글 삭제 실패: " + e.getMessage()));
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new ResponseDto<>(-1, "서버 오류: " + e.getMessage()));
+ }
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/controller/StoreController.java b/application/src/main/java/com/groom/yummy/controller/StoreController.java
new file mode 100644
index 0000000..3550275
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/controller/StoreController.java
@@ -0,0 +1,43 @@
+package com.groom.yummy.controller;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.external.Category;
+import com.groom.yummy.external.StoreApiClient;
+import com.groom.yummy.external.dto.ApiResponse;
+import com.groom.yummy.external.dto.StoreListResponse;
+import com.groom.yummy.external.dto.StoreResponseDto;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "[Store] Store API")
+@RestController
+@RequestMapping("/api/v1/stores")
+@RequiredArgsConstructor
+public class StoreController {
+ private final StoreApiClient storeApiClient;
+
+ @Operation(summary = "가게 정보 조회", description = "가게 id로 가게를 조회합니다.")
+ @GetMapping("/{storeId}")
+ public ResponseEntity> getStore(@PathVariable("storeId") Long storeId) {
+ StoreResponseDto storeResponseDTO = storeApiClient.getStoreByApi(storeId);
+ return ResponseEntity.ok(ResponseDto.of(storeResponseDTO,"storeId로 가게 조회 성공"));
+ }
+
+ @Operation(summary = "가게 조회", description = "가게들을 정렬 기준에 맞게 조회합니다.")
+ @GetMapping
+ public ResponseEntity> getStoresByFilters(
+ @RequestParam(name = "category", required = false) Category category,
+ @RequestParam(name = "regionId", required = false) Long regionId,
+ @RequestParam(name = "name", required = false) String name,
+ @RequestParam(name = "page", defaultValue = "1") int page,
+ @RequestParam(name = "size", defaultValue = "10") int size) {
+ ApiResponse storeListResponseApiResponse = storeApiClient.getStoresByFilters(category, regionId,name,page,size);
+ return ResponseEntity.ok(storeListResponseApiResponse);
+
+ }
+}
+
+
diff --git a/application/src/main/java/com/groom/yummy/controller/UserController.java b/application/src/main/java/com/groom/yummy/controller/UserController.java
new file mode 100644
index 0000000..7f2ec7b
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/controller/UserController.java
@@ -0,0 +1,56 @@
+package com.groom.yummy.controller;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.user.dto.request.UpdateNicknameReqDto;
+import com.groom.yummy.user.dto.response.UserInfoResDto;
+import com.groom.yummy.user.facade.UserFacade;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "[User] User API")
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/api/v1/users")
+public class UserController {
+ private final UserFacade userFacade;
+
+ @Operation(summary = "자신의 정보 조회", description = "토큰 정보 기반 유저 정보를 조회합니다")
+ @GetMapping()
+ public ResponseEntity> getUserInfoByToken(@AuthenticationPrincipal LoginUser loginUser){
+ Long userId = loginUser.getUserId();
+ UserInfoResDto userInfoResDto = userFacade.getUserInfo(userId);
+ return ResponseEntity.ok(ResponseDto.of(userInfoResDto,"자신의 정보 조회 성공"));
+ }
+
+ @Operation(summary = "유저 정보 조회", description = "토큰 정보 기반 유저 정보를 조회합니다")
+ @GetMapping("/{userId}")
+ public ResponseEntity> getUserInfo(@PathVariable("userId") Long userId){
+ UserInfoResDto userInfoResDto = userFacade.getUserInfo(userId);
+ return ResponseEntity.ok(ResponseDto.of(userInfoResDto,"유저 정보 조회 성공"));
+ }
+
+ @Operation(summary = "유저 닉네임 변경", description = "토큰 정보 기반 유저 닉네임을 변경합니다.")
+ @PatchMapping("/profile")
+ public ResponseEntity> updateUserNickname(@Valid @RequestBody UpdateNicknameReqDto updateNicknameReqDto,
+ @AuthenticationPrincipal LoginUser loginUser){
+ Long userId = loginUser.getUserId();
+ UserInfoResDto userInfoResDto = userFacade.updateUserNickname(userId,updateNicknameReqDto);
+ return ResponseEntity.ok(ResponseDto.of(userInfoResDto,"유저 닉네임 변경 성공"));
+ }
+
+ @Operation(summary = "유저 계정 삭제", description = "토큰 정보 기반 유저를 삭제합니다.")
+ @DeleteMapping()
+ public ResponseEntity> deleteUserByToken(@AuthenticationPrincipal LoginUser loginUser){
+ Long userId = loginUser.getUserId();
+ Long deleteUserId = userFacade.deleteUser(userId);
+ return ResponseEntity.ok(ResponseDto.of(deleteUserId,"회원정보 삭제 성공"));
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/exception/JwtErrorCode.java b/application/src/main/java/com/groom/yummy/exception/JwtErrorCode.java
new file mode 100644
index 0000000..1be3c08
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/exception/JwtErrorCode.java
@@ -0,0 +1,20 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum JwtErrorCode implements ErrorCode {
+ WRONG_TYPE_TOKEN(HttpStatus.UNAUTHORIZED,"토큰의 서명이 유효하지 않습니다."),
+ UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED,"잘못된 형식의 토큰입니다."),
+ EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED,"만료된 토큰입니다."),
+ UNKNOWN_TOKEN_ERROR(HttpStatus.BAD_REQUEST,"토큰의 값이 존재하지 않습니다."),
+ ;
+ private final HttpStatus code;
+ private final String message;
+
+ JwtErrorCode(HttpStatus code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/exception/handler/RestControllerExceptionHandler.java b/application/src/main/java/com/groom/yummy/exception/handler/RestControllerExceptionHandler.java
new file mode 100644
index 0000000..49c3449
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/exception/handler/RestControllerExceptionHandler.java
@@ -0,0 +1,46 @@
+package com.groom.yummy.exception.handler;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.exception.CustomException;
+import io.swagger.v3.oas.annotations.Hidden;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@Hidden
+@RestControllerAdvice
+public class RestControllerExceptionHandler {
+
+ /* 컨트롤러 메서드의 파라미터나 엔터티 필드에서 유효성 검사가 실패 */
+ @ExceptionHandler(ConstraintViolationException.class)
+ public ResponseEntity> handleConstraintViolationException(ConstraintViolationException ex){
+ log.error("유효성 검사 예외 발생 msg: {}", ex.getMessage());
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.of(-1, ex.getMessage()));
+ }
+
+ /* @RequestBody 로 들어오는 JSON 요청 객체의 필드 값이 유효성 검사에 실패 */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex){
+ BindingResult bindingResult = ex.getBindingResult();
+ FieldError fieldError = bindingResult.getFieldError();
+ String message = fieldError.getDefaultMessage();
+ log.error("유효성 검사 예외 발생 msg:{}",message);
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.of(-1,message));
+ }
+
+ /* 클라이언트에서 잘못된 JSON 형식의 요청을 보낼 때 발생 */
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
+ String errorMessage = "요청한 JSON 데이터를 읽을 수 없습니다: " + ex.getMessage();
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.of(-1,errorMessage));
+ }
+}
+
diff --git a/application/src/main/java/com/groom/yummy/exception/security/CustomAccessDeniedHandler.java b/application/src/main/java/com/groom/yummy/exception/security/CustomAccessDeniedHandler.java
new file mode 100644
index 0000000..ff257de
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/exception/security/CustomAccessDeniedHandler.java
@@ -0,0 +1,26 @@
+package com.groom.yummy.exception.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.dto.ResponseDto;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class CustomAccessDeniedHandler implements AccessDeniedHandler {
+ private final ObjectMapper objectMapper;
+ @Override
+ public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
+ response.setContentType("application/json;charset=UTF-8");
+ String result = objectMapper.writeValueAsString(ResponseDto.of(403,"접근 권한이 없는 사용자입니다."));
+ response.setStatus(403);
+ response.getWriter().write(result);
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/exception/security/CustomAuthenticationEntryPoint.java b/application/src/main/java/com/groom/yummy/exception/security/CustomAuthenticationEntryPoint.java
new file mode 100644
index 0000000..4977aae
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/exception/security/CustomAuthenticationEntryPoint.java
@@ -0,0 +1,26 @@
+package com.groom.yummy.exception.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.dto.ResponseDto;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ private final ObjectMapper objectMapper;
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+ response.setContentType("application/json;charset=UTF-8");
+ String result = objectMapper.writeValueAsString(ResponseDto.of(401,"인증이 실패하였습니다."));
+ response.setStatus(401);
+ response.getWriter().write(result);
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/external/Category.java b/application/src/main/java/com/groom/yummy/external/Category.java
new file mode 100644
index 0000000..9b20632
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/Category.java
@@ -0,0 +1,27 @@
+package com.groom.yummy.external;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum Category {
+
+ CHICKEN("치킨"),
+ CHINESE("중식"),
+ CUTLET_SASHIMI("돈까스-회"),
+ PIZZA("피자"),
+ FAST_FOOD("패스트푸드"),
+ STEW_SOUP("찜-탕"),
+ JOKBAL_BOSSAM("족발-보쌈"),
+ SNACK("분식"),
+ CAFE_DESSERT("카페-디저트"),
+ KOREAN("한식"),
+ MEAT("고기"),
+ WESTERN("양식"),
+ ASIAN("아시안"),
+ LATE_NIGHT("야식"),
+ LUNCH_BOX("도시락");
+ private final String description;
+
+}
diff --git a/application/src/main/java/com/groom/yummy/external/StoreApiClient.java b/application/src/main/java/com/groom/yummy/external/StoreApiClient.java
new file mode 100644
index 0000000..093d841
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/StoreApiClient.java
@@ -0,0 +1,43 @@
+package com.groom.yummy.external;
+
+import com.groom.yummy.external.dto.ApiResponse;
+import com.groom.yummy.external.dto.StoreListResponse;
+import com.groom.yummy.external.dto.StoreResponseDto;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Service
+@RequiredArgsConstructor
+public class StoreApiClient {
+ private final WebClient webClient;
+
+ public StoreResponseDto getStoreByApi(Long storeId){
+ StoreResponseDto storeResponse = webClient.get()
+ .uri(uriBuilder -> uriBuilder.path("/api/v1/store/{id}")
+ .build(storeId))
+ .retrieve()
+ .bodyToMono(new ParameterizedTypeReference>() {})
+ .block().data();
+
+ return storeResponse;
+ }
+
+ public ApiResponse getStoresByFilters(
+ Category category, Long regionId, String name, int page, int size) {
+ return webClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/api/v1/store")
+ .queryParam("category", category)
+ .queryParam("regionId", regionId)
+ .queryParam("name", name)
+ .queryParam("page", page)
+ .queryParam("size", size)
+ .build())
+ .retrieve()
+ .bodyToMono(new ParameterizedTypeReference>() {})
+ .block();
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/external/dto/ApiResponse.java b/application/src/main/java/com/groom/yummy/external/dto/ApiResponse.java
new file mode 100644
index 0000000..1177aa8
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/dto/ApiResponse.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.external.dto;
+
+public record ApiResponse(
+ T data,
+ String message
+) {}
diff --git a/application/src/main/java/com/groom/yummy/external/dto/PaginationInfo.java b/application/src/main/java/com/groom/yummy/external/dto/PaginationInfo.java
new file mode 100644
index 0000000..36de235
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/dto/PaginationInfo.java
@@ -0,0 +1,9 @@
+package com.groom.yummy.external.dto;
+
+public record PaginationInfo(
+ int page,
+ int size,
+ int totalPages,
+ long totalElements
+) {
+}
diff --git a/application/src/main/java/com/groom/yummy/external/dto/StoreListResponse.java b/application/src/main/java/com/groom/yummy/external/dto/StoreListResponse.java
new file mode 100644
index 0000000..951a967
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/dto/StoreListResponse.java
@@ -0,0 +1,9 @@
+package com.groom.yummy.external.dto;
+
+import java.util.List;
+
+
+public record StoreListResponse(
+ List stores,
+ PaginationInfo pageable
+) {}
diff --git a/application/src/main/java/com/groom/yummy/external/dto/StoreRequestDto.java b/application/src/main/java/com/groom/yummy/external/dto/StoreRequestDto.java
new file mode 100644
index 0000000..1cc8ea0
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/dto/StoreRequestDto.java
@@ -0,0 +1,14 @@
+package com.groom.yummy.external.dto;
+
+
+import lombok.Builder;
+import com.groom.yummy.external.Category;
+
+
+@Builder
+public record StoreRequestDto(
+ String name,
+ Category category,
+ Long regionId
+) {
+}
diff --git a/application/src/main/java/com/groom/yummy/external/dto/StoreResponseDto.java b/application/src/main/java/com/groom/yummy/external/dto/StoreResponseDto.java
new file mode 100644
index 0000000..5fd39e1
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/external/dto/StoreResponseDto.java
@@ -0,0 +1,13 @@
+package com.groom.yummy.external.dto;
+
+
+import lombok.Builder;
+
+@Builder
+public record StoreResponseDto(
+ Long id,
+ String name,
+ String category,
+ Long regionId
+) {
+}
diff --git a/application/src/main/java/com/groom/yummy/filter/JwtAuthFilter.java b/application/src/main/java/com/groom/yummy/filter/JwtAuthFilter.java
new file mode 100644
index 0000000..572cc28
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/filter/JwtAuthFilter.java
@@ -0,0 +1,120 @@
+package com.groom.yummy.filter;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.ErrorCode;
+import com.groom.yummy.exception.JwtErrorCode;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.user.User;
+import com.groom.yummy.jwt.JwtProvider;
+import io.jsonwebtoken.Jwt;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+import static com.groom.yummy.exception.JwtErrorCode.*;
+
+@Log4j2
+@Component
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+ private final JwtProvider jwtProvider;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ try{
+ String token = findToken(request);
+ if (!verifyToken(request, token)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ User user = getUser(token);
+ setSecuritySession(user);
+ filterChain.doFilter(request, response);
+
+ }catch (CustomException e){
+ JwtErrorCode jwtErrorCode = (JwtErrorCode) e.getErrorCode();
+ switch (jwtErrorCode) {
+ case WRONG_TYPE_TOKEN, UNSUPPORTED_TOKEN, EXPIRED_TOKEN, UNKNOWN_TOKEN_ERROR ->
+ setResponse(response, jwtErrorCode);
+ default -> {
+ log.error("알 수 없는 에러 코드: {}", jwtErrorCode);
+ setResponse(response, UNKNOWN_TOKEN_ERROR); // 기본 예외 처리
+ }
+ }
+ }
+ }
+
+ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
+ String path = request.getRequestURI();
+ return path.startsWith("/swagger-ui/") || path.startsWith("/v3/api-docs") || path.startsWith("/swagger-resources/");
+ }
+
+ private static void setSecuritySession(User user){
+ LoginUser loginUser = new LoginUser(user);
+ log.info("SessionLoginUser : {}", loginUser.getUsername());
+ log.info("SessionLoginUser getAuthorities: {}", loginUser.getAuthorities());
+ Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+
+ private User getUser(String token){
+ Long userId = jwtProvider.getUserId(token);
+ String username = jwtProvider.getUsername(token);
+ String name = jwtProvider.getName(token);
+ String role = jwtProvider.getRole(token);
+
+ log.info("getUser username: ", username);
+ return User.builder()
+ .id(userId)
+ .email(username)
+ .nickname(name)
+ .role(role)
+ .build();
+ }
+
+ private boolean verifyToken(HttpServletRequest request,String token) throws IOException, ServletException {
+ Boolean isValid = (Boolean) request.getAttribute("isTokenValid");
+ if(isValid != null) return isValid;
+
+ if (token == null || jwtProvider.validateToken(token)) {
+ log.debug("token null");
+ request.setAttribute("isTokenValid",false);
+ return false;
+ }
+
+ request.setAttribute("isTokenValid", true);
+ return true;
+ }
+
+ private static String findToken(HttpServletRequest request){
+ String token = null;
+ Cookie[] cookies = request.getCookies();
+ for(Cookie cookie : cookies){
+ if(cookie.getName().equals("Authorization")){
+ token = cookie.getValue();
+ }
+ }
+ return token;
+ }
+ private void setResponse(HttpServletResponse response, JwtErrorCode jwtErrorCode) throws IOException {
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(jwtErrorCode.getCode().value());
+ response.getWriter().print(objectMapper.writeValueAsString(ResponseDto.of(-1,jwtErrorCode.getMessage())));
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/jwt/JwtProvider.java b/application/src/main/java/com/groom/yummy/jwt/JwtProvider.java
new file mode 100644
index 0000000..bd75bcb
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/jwt/JwtProvider.java
@@ -0,0 +1,84 @@
+package com.groom.yummy.jwt;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.JwtErrorCode;
+import io.jsonwebtoken.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+@Component
+@Slf4j
+public class JwtProvider {
+ private SecretKey secretKey;
+
+ @Value("${spring.jwt.valid-time}")
+ public Long VALID_TIME;
+
+ @Value("${spring.jwt.cookie-name}")
+ public String COOKIE_NAME;
+
+ private JwtProvider(@Value("${spring.jwt.secret}") String secret){
+ secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
+ }
+
+ public String createAccessToken(Long userId, String email, String nickname, String role){
+ Date timeNow = new Date(System.currentTimeMillis());
+ Date expirationTime = new Date(timeNow.getTime() + VALID_TIME);
+
+ return Jwts.builder()
+ .claim("userId", userId)
+ .claim("email",email)
+ .claim("nickname", nickname)
+ .claim("role",role)
+ .setIssuedAt(timeNow)
+ .setExpiration(expirationTime)
+ .signWith(secretKey, SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ public Long getUserId(String token) {
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("userId", Long.class);
+ }
+ public String getUsername(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class);
+ }
+
+ public String getName(String token){
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("nickname", String.class);
+ }
+
+ public String getRole(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
+ }
+
+ public boolean validateToken(String token){
+ //log.info("토큰 유효성 검증 시작");
+ return valid(secretKey, token);
+ }
+
+ private boolean valid(SecretKey secretKey, String token){
+ try{
+ Jws claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
+ return claims.getBody().getExpiration().before(new Date());
+ }catch (SignatureException ex){
+ throw new CustomException(JwtErrorCode.WRONG_TYPE_TOKEN);
+ }catch (MalformedJwtException ex){
+ throw new CustomException(JwtErrorCode.UNSUPPORTED_TOKEN);
+ }catch (ExpiredJwtException ex){
+ throw new CustomException(JwtErrorCode.EXPIRED_TOKEN);
+ }catch (IllegalArgumentException ex){
+ throw new CustomException(JwtErrorCode.WRONG_TYPE_TOKEN);
+ }
+ }
+
+}
+
+
diff --git a/application/src/main/java/com/groom/yummy/oauth2/auth/LoginUser.java b/application/src/main/java/com/groom/yummy/oauth2/auth/LoginUser.java
new file mode 100644
index 0000000..b443319
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/auth/LoginUser.java
@@ -0,0 +1,56 @@
+package com.groom.yummy.oauth2.auth;
+
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import com.groom.yummy.user.User;
+
+import java.util.Collection;
+import java.util.Map;
+
+@Getter
+public class LoginUser implements UserDetails, OAuth2User{
+ private final User user;
+ private Map attributes;
+
+ public LoginUser(User user){
+ this.user = user;
+ }
+ public LoginUser(User user, Map attributes) {
+ this.user = user;
+ this.attributes = attributes;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return null;
+ }
+
+
+ @Override
+ public String getUsername() {
+ return user.getEmail();
+ }
+
+ @Override
+ public String getName() {
+ return user.getNickname();
+ }
+
+ public String getRole(){
+ return user.getRole();
+ }
+
+ public Long getUserId(){return user.getId();}
+
+ @Override
+ public String getPassword() {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/oauth2/dto/KakaoResponse.java b/application/src/main/java/com/groom/yummy/oauth2/dto/KakaoResponse.java
new file mode 100644
index 0000000..1ef3b63
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/dto/KakaoResponse.java
@@ -0,0 +1,33 @@
+package com.groom.yummy.oauth2.dto;
+
+
+import java.util.Map;
+
+public class KakaoResponse implements OAuth2Response {
+
+ private final Map attributes;
+
+ public KakaoResponse(Map attributes) {
+ this.attributes = attributes;
+ }
+
+ @Override
+ public String getProvider() {
+ return "kakao";
+ }
+
+ @Override
+ public String getProviderId() {
+ return attributes.get("id").toString();
+ }
+
+ @Override
+ public String getEmail() {
+ return ((Map)attributes.get("kakao_account")).get("email").toString();
+ }
+
+ @Override
+ public String getName() {
+ return ((Map)attributes.get("properties")).get("nickname").toString();
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/oauth2/dto/OAuth2Response.java b/application/src/main/java/com/groom/yummy/oauth2/dto/OAuth2Response.java
new file mode 100644
index 0000000..8bf578c
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/dto/OAuth2Response.java
@@ -0,0 +1,8 @@
+package com.groom.yummy.oauth2.dto;
+
+public interface OAuth2Response {
+ String getProvider();
+ String getProviderId();
+ String getEmail();
+ String getName();
+}
diff --git a/application/src/main/java/com/groom/yummy/oauth2/handler/CustomSuccessHandler.java b/application/src/main/java/com/groom/yummy/oauth2/handler/CustomSuccessHandler.java
new file mode 100644
index 0000000..98d50e4
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/handler/CustomSuccessHandler.java
@@ -0,0 +1,43 @@
+package com.groom.yummy.oauth2.handler;
+
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.util.CookieUtil;
+import com.groom.yummy.jwt.JwtProvider;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class CustomSuccessHandler implements AuthenticationSuccessHandler {
+
+ private final JwtProvider jwtProvider;
+
+ @Value("${server.url}")
+ private String SERVER_URL;
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+ LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+ log.info("onAuthenticationSuccess Authenticated User: " + loginUser.getName());
+ String email = loginUser.getUsername();
+ String nickname = loginUser.getName();
+ Long userId = loginUser.getUserId();
+ String role = loginUser.getRole();
+
+ String accessToken = jwtProvider.createAccessToken(userId,email,nickname,role);
+
+ response.addCookie(CookieUtil.createCookie(jwtProvider.COOKIE_NAME,accessToken, jwtProvider.VALID_TIME));
+ response.sendRedirect(SERVER_URL+"/swagger-ui/index.html");
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/oauth2/service/CustomOAuth2UserService.java b/application/src/main/java/com/groom/yummy/oauth2/service/CustomOAuth2UserService.java
new file mode 100644
index 0000000..7e52e96
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/service/CustomOAuth2UserService.java
@@ -0,0 +1,46 @@
+package com.groom.yummy.oauth2.service;
+
+import com.groom.yummy.user.facade.UserFacade;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.oauth2.dto.OAuth2Response;
+import com.groom.yummy.oauth2.strategy.OAuth2ResFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+import com.groom.yummy.user.User;
+
+import java.util.Optional;
+
+@Service
+@Slf4j
+public class CustomOAuth2UserService extends DefaultOAuth2UserService {
+
+ private final OAuth2ResFactory oAuth2ResFactory;
+ private final UserFacade userFacade;
+
+ public CustomOAuth2UserService(OAuth2ResFactory oAuth2ResFactory, UserFacade userFacade) {
+ this.oAuth2ResFactory = oAuth2ResFactory;
+ this.userFacade = userFacade;
+ }
+
+ @Override
+ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
+ OAuth2User oAuth2User = super.loadUser(userRequest);
+ log.info("oAuth2User key-set : {}", oAuth2User.getAttributes().keySet());
+ System.out.println("ID: " + oAuth2User.getAttributes().get("id"));
+ System.out.println("Connected At: " + oAuth2User.getAttributes().get("connected_at"));
+ String registrationId = userRequest.getClientRegistration().getRegistrationId();
+
+ OAuth2Response oAuth2Response = oAuth2ResFactory.createOAuth2Response(registrationId, oAuth2User.getAttributes());
+ String email = oAuth2Response.getEmail();
+ String nickname = oAuth2Response.getName();
+
+ Optional optionalUser = userFacade.findAuthUserByEmail(email);
+ User user = userFacade.findOrCreateUser(optionalUser, nickname, email);
+ log.info("loadUser : {}", user.getEmail());
+ return new LoginUser(user, oAuth2User.getAttributes());
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/oauth2/strategy/KakaoOAuth2ResStrategy.java b/application/src/main/java/com/groom/yummy/oauth2/strategy/KakaoOAuth2ResStrategy.java
new file mode 100644
index 0000000..f2f8a9d
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/strategy/KakaoOAuth2ResStrategy.java
@@ -0,0 +1,21 @@
+package com.groom.yummy.oauth2.strategy;
+
+import com.groom.yummy.oauth2.dto.KakaoResponse;
+import com.groom.yummy.oauth2.dto.OAuth2Response;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class KakaoOAuth2ResStrategy implements OAuth2ResStrategy{
+
+ @Override
+ public String getProviderName() {
+ return "kakao";
+ }
+
+ @Override
+ public OAuth2Response createOAuth2Response(Map attributes) {
+ return new KakaoResponse(attributes);
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResFactory.java b/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResFactory.java
new file mode 100644
index 0000000..8473f9e
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResFactory.java
@@ -0,0 +1,26 @@
+package com.groom.yummy.oauth2.strategy;
+
+import com.groom.yummy.oauth2.dto.OAuth2Response;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Component
+public class OAuth2ResFactory {
+ private final Map strategies;
+
+ public OAuth2ResFactory(List strategiyList) {
+ this.strategies = strategiyList.stream()
+ .collect(Collectors.toMap(OAuth2ResStrategy::getProviderName, Function.identity()));
+ }
+
+ public OAuth2Response createOAuth2Response(String registrationId, Map attributes){
+ OAuth2ResStrategy strategy = strategies.get(registrationId);
+ System.out.println(strategy);
+ if(Objects.equals(strategy,null)) throw new IllegalStateException("지원하지 않는 provider 입니다.");
+ return strategy.createOAuth2Response(attributes);
+ }}
diff --git a/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResStrategy.java b/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResStrategy.java
new file mode 100644
index 0000000..b10d05a
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/oauth2/strategy/OAuth2ResStrategy.java
@@ -0,0 +1,10 @@
+package com.groom.yummy.oauth2.strategy;
+
+import com.groom.yummy.oauth2.dto.OAuth2Response;
+
+import java.util.Map;
+
+public interface OAuth2ResStrategy {
+ String getProviderName();
+ OAuth2Response createOAuth2Response(Map attributes);
+}
diff --git a/application/src/main/java/com/groom/yummy/reply/dto/ReplyRequestDto.java b/application/src/main/java/com/groom/yummy/reply/dto/ReplyRequestDto.java
new file mode 100644
index 0000000..180f52d
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/reply/dto/ReplyRequestDto.java
@@ -0,0 +1,30 @@
+package com.groom.yummy.reply.dto;
+
+import com.groom.yummy.reply.Reply;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+public record ReplyRequestDto(
+ @NotNull(message = "댓글 내용은 필수입니다.")
+ @Size(max = 500, message = "댓글 내용은 500자를 넘을 수 없습니다.")
+ String content,
+
+ Long parentId,
+
+ @NotNull(message = "작성자 ID는 필수입니다.")
+ Long userId,
+
+ @NotNull(message = "그룹 ID는 필수입니다.")
+ Long groupId
+) {
+
+ public Reply toDomain() {
+ return Reply.builder()
+ .content(content)
+ .parentReplyId(parentId)
+ .userId(userId)
+ .groupId(groupId)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/reply/dto/ReplyResponseDto.java b/application/src/main/java/com/groom/yummy/reply/dto/ReplyResponseDto.java
new file mode 100644
index 0000000..e6f55d7
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/reply/dto/ReplyResponseDto.java
@@ -0,0 +1,31 @@
+package com.groom.yummy.reply.dto;
+
+import java.time.LocalDateTime;
+
+import com.groom.yummy.reply.Reply;
+
+import lombok.Builder;
+
+@Builder
+public record ReplyResponseDto(
+ Long id,
+ String content,
+ Long parentId,
+ Long userId,
+ Long groupId,
+ LocalDateTime createdAt,
+ LocalDateTime updatedAt
+) {
+
+ public static ReplyResponseDto fromDomain(Reply reply) {
+ return ReplyResponseDto.builder()
+ .id(reply.getId())
+ .content(reply.getContent())
+ .parentId(reply.getParentReplyId())
+ .userId(reply.getUserId())
+ .groupId(reply.getGroupId())
+ .createdAt(reply.getCreatedAt())
+ .updatedAt(reply.getUpdatedAt())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/reply/dto/ReplyUpdateRequestDto.java b/application/src/main/java/com/groom/yummy/reply/dto/ReplyUpdateRequestDto.java
new file mode 100644
index 0000000..88d8b00
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/reply/dto/ReplyUpdateRequestDto.java
@@ -0,0 +1,13 @@
+package com.groom.yummy.reply.dto;
+
+import com.groom.yummy.reply.Reply;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+public record ReplyUpdateRequestDto(
+ @NotNull(message = "댓글 내용은 필수입니다.")
+ @Size(max = 500, message = "댓글 내용은 500자를 넘을 수 없습니다.")
+ String content
+) {
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/resolver/AuthUserResolver.java b/application/src/main/java/com/groom/yummy/resolver/AuthUserResolver.java
new file mode 100644
index 0000000..a8bd0e3
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/resolver/AuthUserResolver.java
@@ -0,0 +1,33 @@
+//package com.groom.yummy.resolver;
+//
+//import com.groom.yummy.domain.user.User;
+//import com.groom.yummy.domain.user.UserAuthService;
+//import lombok.RequiredArgsConstructor;
+//import org.springframework.core.MethodParameter;
+//import org.springframework.security.core.Authentication;
+//import org.springframework.security.core.context.SecurityContextHolder;
+//import org.springframework.web.bind.support.WebDataBinderFactory;
+//import org.springframework.web.context.request.NativeWebRequest;
+//import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+//import org.springframework.web.method.support.ModelAndViewContainer;
+//
+//@RequiredArgsConstructor
+//public class AuthUserResolver implements HandlerMethodArgumentResolver {
+//
+// private final UserAuthService userAuthService;
+//
+// @Override
+// public boolean supportsParameter(MethodParameter parameter) {
+// return User.class.isAssignableFrom(parameter.getParameterType());
+// }
+//
+// @Override
+// public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+// NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
+// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+// if(auth == null){
+// throw new RuntimeException();
+// }
+// return userAuthService.findAuthUserByEmail(auth.getName()).orElseThrow(RuntimeException::new);
+// }
+//}
diff --git a/application/src/main/java/com/groom/yummy/test/TestCreateController.java b/application/src/main/java/com/groom/yummy/test/TestCreateController.java
new file mode 100644
index 0000000..c53ee53
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/test/TestCreateController.java
@@ -0,0 +1,79 @@
+package com.groom.yummy.test;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.jwt.JwtProvider;
+import com.groom.yummy.test.dto.SignInDto;
+import com.groom.yummy.test.dto.TestUserCreateDto;
+import com.groom.yummy.test.dto.TestUserResDto;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Optional;
+
+import static org.springframework.http.HttpHeaders.SET_COOKIE;
+
+@Tag(name = "[TestUser] TestUser API")
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/api/v1/test/users")
+public class TestCreateController {
+
+ private final JwtProvider jwtProvider;
+ private final UserRepository userRepository;
+
+ @Operation(summary = "회원가입", description = "회원 가입 할 유저의 정보를 입력합니다. " +
+ "
회원가입이 완료되면 자동으로 유저의 기본 profile 사진이 등록됩니다." +
+ "
이후에 유저의 Token 을 통해 profile 사진을 수정할 수 있습니다.")
+ @PostMapping("/sign-up")
+ public ResponseEntity> signUp(@Valid @RequestBody TestUserCreateDto testUserCreateDto){
+ User user = User.builder()
+ .email(testUserCreateDto.email())
+ .nickname(testUserCreateDto.nickname())
+ .role("ROLE_USER")
+ .groupJoinCount(0L)
+ .groupAttendanceCount(0L)
+ .isDeleted(false)
+ .build();
+
+ user = userRepository.save(user);
+ TestUserResDto testUserResDto = TestUserResDto.from(user);
+ return ResponseEntity.status(HttpStatus.CREATED).body(
+ new ResponseDto<>(
+ testUserResDto,
+ "테스트 유저 생성 성공"));
+ }
+
+
+ @Operation(summary = "로그인", description = "회원 가입 한 유저의 loginId와 password를 입력합니다.")
+ @PostMapping("/sign-in")
+ public ResponseEntity> signIn(@Valid @RequestBody SignInDto signInReqDto){
+
+ Optional optionalUser = userRepository.findByEmail(signInReqDto.email());
+ User user = optionalUser.get();
+ String accessToken = jwtProvider.createAccessToken(user.getId(), user.getEmail(),user.getNickname(),user.getRole());
+ ResponseCookie cookie = ResponseCookie.from("Authorization", accessToken)
+ .httpOnly(true)
+ .secure(true)
+ .path("/")
+ .maxAge(7200000)
+ .sameSite("Strict")
+ .build();
+
+ return ResponseEntity.status(HttpStatus.OK)
+ .header(SET_COOKIE, cookie.toString())
+ .body(new ResponseDto<>(1, "성공하였습니당."));
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/test/dto/SignInDto.java b/application/src/main/java/com/groom/yummy/test/dto/SignInDto.java
new file mode 100644
index 0000000..1a04d9d
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/test/dto/SignInDto.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.test.dto;
+
+public record SignInDto(
+ String email
+) {
+}
diff --git a/application/src/main/java/com/groom/yummy/test/dto/TestUserCreateDto.java b/application/src/main/java/com/groom/yummy/test/dto/TestUserCreateDto.java
new file mode 100644
index 0000000..5636750
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/test/dto/TestUserCreateDto.java
@@ -0,0 +1,7 @@
+package com.groom.yummy.test.dto;
+
+public record TestUserCreateDto(
+ String nickname,
+ String email
+) {
+}
diff --git a/application/src/main/java/com/groom/yummy/test/dto/TestUserResDto.java b/application/src/main/java/com/groom/yummy/test/dto/TestUserResDto.java
new file mode 100644
index 0000000..5ee953b
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/test/dto/TestUserResDto.java
@@ -0,0 +1,23 @@
+package com.groom.yummy.test.dto;
+
+import com.groom.yummy.user.User;
+import lombok.Builder;
+
+@Builder
+public record TestUserResDto(
+ Long id,
+ String email,
+ String nickname,
+ Long joinCount,
+ Long participationCount
+) {
+ public static TestUserResDto from(User user){
+ return TestUserResDto.builder()
+ .id(user.getId())
+ .email(user.getEmail())
+ .nickname(user.getNickname())
+ .joinCount(user.getGroupJoinCount())
+ .participationCount(user.getGroupAttendanceCount())
+ .build();
+ }
+}
diff --git a/application/src/main/java/com/groom/yummy/util/CookieUtil.java b/application/src/main/java/com/groom/yummy/util/CookieUtil.java
new file mode 100644
index 0000000..0f24395
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/util/CookieUtil.java
@@ -0,0 +1,31 @@
+package com.groom.yummy.util;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+
+public class CookieUtil {
+ public static Cookie createCookie(final String nameOfCookie, final String token,
+ final Long cookieValidTime) {
+ Cookie cookie = new Cookie(nameOfCookie,token);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(Math.toIntExact(cookieValidTime));
+ cookie.setSecure(true);
+ return cookie;
+ }
+
+ public static Cookie getCookie(HttpServletRequest request, String nameOfCookie) {
+ Cookie[] cookies = request.getCookies();
+
+ if (cookies == null || cookies.length == 0) {
+ return null;
+ }
+
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(nameOfCookie)) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/com/groom/yummy/webclient/SomeApiService.java b/application/src/main/java/com/groom/yummy/webclient/SomeApiService.java
new file mode 100755
index 0000000..668e8b9
--- /dev/null
+++ b/application/src/main/java/com/groom/yummy/webclient/SomeApiService.java
@@ -0,0 +1,69 @@
+package com.groom.yummy.webclient;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.store.Category_;
+import com.groom.yummy.store.Store;
+import com.groom.yummy.store.StoreService;
+import com.groom.yummy.store.dto.StoreApiResponseDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+@Service
+@RequiredArgsConstructor
+public class SomeApiService {
+ private final WebClient.Builder webClientBuilder;
+ private final StoreService storeService; // StoreService 주입
+
+ public List fetchStoresFromApi(String regionCode) {
+ WebClient webClient = webClientBuilder.build();
+ try {
+ String response = webClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/api/v1/stores")
+ .queryParam("regionCode", regionCode)
+ .build())
+ .retrieve()
+ .bodyToMono(String.class)
+ .block();
+
+ // JSON 응답을 파싱하여 StoreApiResponseDto 리스트 생성
+ List storeDtos = parseStoresFromResponse(response);
+
+ // Store 객체로 변환 후 StoreService에 저장 요청
+ storeDtos.forEach(dto -> {
+ Store store = Store.builder()
+ .storeId(dto.getStoreId())
+ .name(dto.getName())
+ .category(Category_.fromApiCode(dto.getCategory()))
+ .regionId(dto.getRegionId())
+ .build();
+ storeService.createStore(store);
+ });
+
+ return storeDtos; // 반환값 추가
+ } catch (Exception e) {
+ throw new RuntimeException("WebClient 요청 중 오류 발생", e);
+ }
+ }
+
+
+ private List parseStoresFromResponse(String response) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ try {
+ JsonNode storesNode = objectMapper.readTree(response).path("data").path("stores");
+ return StreamSupport.stream(storesNode.spliterator(), false)
+ .map(StoreApiResponseDto::fromJsonNode)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ throw new RuntimeException("JSON 파싱 중 오류 발생", e);
+ }
+ }
+}
+
+
diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml
new file mode 100644
index 0000000..df3f7a1
--- /dev/null
+++ b/application/src/main/resources/application.yml
@@ -0,0 +1,139 @@
+spring:
+ datasource:
+ url: jdbc:mysql://localhost:3306/yummy_db
+ username: yummy
+ password: yummy1234!
+ driver-class-name: com.mysql.cj.jdbc.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: create
+ show-sql: true
+ open-in-view: false
+ properties:
+ hibernate:
+ format_sql: true
+ jdbc:
+ time_zone: Asia/Seoul
+
+ security:
+ oauth2:
+ client:
+ provider:
+ kakao:
+ authorization-uri: https://kauth.kakao.com/oauth/authorize
+ token-uri: https://kauth.kakao.com/oauth/token
+ user-info-uri: https://kapi.kakao.com/v2/user/me
+ user-name-attribute: id
+ registration:
+ kakao:
+ client-name: kakao
+ client-id: ${client-id}
+ client-secret: ${client-secret}
+ redirect-uri: ${redirect-uri}
+ client-authentication-method: client_secret_post
+ authorization-grant-type: authorization_code
+ scope: profile_nickname, account_email
+
+ jwt:
+ secret: qwdhui83jvnllw133qkjbcaw712jnfe982329H#p219qnf43yj90j32
+ valid-time: 7200000
+ cookie-name: Authorization
+
+server:
+ port: 8081
+ url: http://localhost:8081
+---
+
+spring:
+ config:
+ activate:
+ on-profile: prod
+
+ datasource:
+ url: ${DB_URL}
+ username: ${DB_USERNAME}
+ password: ${DB_PASSWORD}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+ show-sql: true
+ open-in-view: false
+ properties:
+ hibernate:
+ format_sql: true
+ dialect: org.hibernate.dialect.MySQLDialect
+
+ security:
+ oauth2:
+ client:
+ provider:
+ kakao:
+ authorization-uri: https://kauth.kakao.com/oauth/authorize
+ token-uri: https://kauth.kakao.com/oauth/token
+ user-info-uri: https://kapi.kakao.com/v2/user/me
+ user-name-attribute: id
+ registration:
+ kakao:
+ client-name: kakao
+ client-id: ${CLIENT_ID}
+ client-secret: ${CLIENT_SECRET}
+ redirect-uri: ${REDIRECT_URI}
+ client-authentication-method: client_secret_post
+ authorization-grant-type: authorization_code
+ scope: profile_nickname, account_email
+
+ jwt:
+ secret: ${JWT_SECRET}
+ valid-time: 7200000
+ cookie-name: Authorization
+
+server:
+ port: 8080
+ url: https://52.78.217.57.nip.io
+---
+spring:
+ config:
+ activate:
+ on-profile: test
+ datasource:
+ url: jdbc:h2:mem:testdb
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
+ h2:
+ console:
+ enabled: true
+ jpa:
+ hibernate:
+ ddl-auto: create
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.H2Dialect
+ show_sql: true
+ format_sql: true
+ default_batch_fetch_size: 50
+ security:
+ oauth2:
+ client:
+ provider:
+ kakao:
+ authorization-uri: https://kauth.kakao.com/oauth/authorize
+ token-uri: https://kauth.kakao.com/oauth/token
+ user-info-uri: https://kapi.kakao.com/v2/user/me
+ user-name-attribute: id
+ registration:
+ kakao:
+ client-name: kakao
+ client-id: ${client-id}
+ client-secret: ${client-secret}
+ redirect-uri: ${redirect-uri}
+ client-authentication-method: client_secret_post
+ authorization-grant-type: authorization_code
+ scope: profile_nickname, account_email
+ jwt:
+ secret: qwdhui83jvnllw133qkjbcaw712jnfe982329H#p219qnf43yj90j32
+ valid-time: 7200000
+ cookie-name: Authorization
\ No newline at end of file
diff --git a/application/src/test/java/com/groom/yummy/controller/GroupControllerTest.java b/application/src/test/java/com/groom/yummy/controller/GroupControllerTest.java
new file mode 100755
index 0000000..60c5f96
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/controller/GroupControllerTest.java
@@ -0,0 +1,254 @@
+package com.groom.yummy.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.group.Group;
+import com.groom.yummy.group.GroupService;
+import com.groom.yummy.group.MeetingStatus;
+import com.groom.yummy.group.dto.request.CreateGroupRequestDto;
+import com.groom.yummy.group.dto.request.JoinGroupRequestDto;
+import com.groom.yummy.group.dto.response.GroupDetailResponseDto;
+import com.groom.yummy.jwt.JwtProvider;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.user.User;
+import jakarta.servlet.http.Cookie;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(GroupController.class)
+class GroupControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockitoBean
+ private GroupService groupService;
+
+ @MockitoBean
+ private JwtProvider jwtProvider;
+
+ private Cookie authCookie;
+
+ @BeforeEach
+ void setUp() {
+ // Mock User 객체 생성
+ User mockUser = User.builder()
+ .id(1L)
+ .email("testUser@gmail.com")
+ .nickname("testNickname")
+ .role("USER")
+ .build();
+
+ // Mock LoginUser 생성
+ LoginUser mockLoginUser = new LoginUser(mockUser);
+
+ // 인증 객체 생성
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(mockLoginUser, null, List.of())
+ );
+
+ // JWT 검증
+ when(jwtProvider.validateToken("valid.jwt.token")).thenReturn(true);
+ when(jwtProvider.getUsername("valid.jwt.token")).thenReturn("testUser@gmail.com");
+
+ // Authorization 쿠키 생성
+ authCookie = new Cookie("Authorization", "valid.jwt.token");
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("사용자는 소모임을 생성할 수 있다.")
+ void 소모임_생성_테스트() throws Exception {
+ // given
+ CreateGroupRequestDto requestDto = CreateGroupRequestDto.builder()
+ .title("Go to Goorm Store!")
+ .content("yummy yummy yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .meetingDate(LocalDateTime.now())
+ .storeId(10L)
+ .build();
+
+ // Mock groupId 반환값
+ Long mockGroupId = 1L;
+
+ // Mock 그룹 생성 동작
+ when(groupService.createGroup(any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockGroupId);
+
+ // Mock findGroupById 반환값 설정
+ Group mockGroup = Group.builder()
+ .id(mockGroupId)
+ .title(requestDto.getTitle())
+ .content(requestDto.getContent())
+ .maxParticipants(requestDto.getMaxParticipants())
+ .minParticipants(requestDto.getMinParticipants())
+ .currentParticipants(1)
+ .meetingDate(requestDto.getMeetingDate())
+ .meetingStatus(MeetingStatus.OPEN)
+ .storeId(requestDto.getStoreId())
+ .build();
+
+ when(groupService.findGroupById(mockGroupId)).thenReturn(Optional.of(mockGroup));
+
+ String body = objectMapper.writeValueAsString(requestDto);
+
+ // when
+ ResultActions actions = mockMvc.perform(post("/api/v1/groups")
+ .cookie(authCookie)
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body));
+
+ // then
+ actions.andExpectAll(
+ status().isCreated(),
+ jsonPath("$.msg").value("소모임 생성 성공"),
+ jsonPath("$.data.id").value(1L)
+ );
+
+ // 호출 횟수 검증
+ verify(groupService, times(1)).createGroup(any(), any(), any(), any(), any(), any(), any());
+ verify(groupService, times(1)).findGroupById(mockGroupId);
+ }
+
+
+ @Test
+ @Order(2)
+ @DisplayName("사용자는 소모임 목록을 조회할 수 있다.")
+ void 소모임_리스트_조회_테스트() throws Exception {
+ // given
+ Group group = Group.builder()
+ .id(1L)
+ .title("Go to Goorm Store!")
+ .content("yummy yummy yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .currentParticipants(1)
+ .meetingDate(LocalDateTime.now())
+ .meetingStatus(MeetingStatus.OPEN)
+ .storeId(10L)
+ .build();
+
+ when(groupService.getAllGroups(any(), any(), any(), anyInt()))
+ .thenReturn(List.of(group));
+
+ // when
+ ResultActions actions = mockMvc.perform(get("/api/v1/groups")
+ .param("category", "KOREAN")
+ .param("regionCode", "110110110")
+ .param("storeName", "구룸식당")
+ .param("page", "1")
+ .cookie(authCookie));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.msg").value("소모임 목록 조회 성공"),
+ jsonPath("$.data[0].id").value(1L),
+ jsonPath("$.data[0].title").value("Go to Goorm Store!")
+ );
+
+ verify(groupService, times(1)).getAllGroups(any(), any(), any(), anyInt());
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("사용자는 소모임 상세 정보를 조회할 수 있다.")
+ void 소모임_상세정보_조회_테스트() throws Exception {
+ // given
+ GroupDetailResponseDto responseDto = GroupDetailResponseDto.builder()
+ .id(1L)
+ .title("Go to Goorm Store!")
+ .content("yummy yummy yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .currentParticipants(1)
+ .meetingDate(LocalDateTime.now())
+ .meetingStatus("OPEN")
+ .storeId(10L)
+ .createdAt(LocalDateTime.now())
+ .build();
+
+ when(groupService.findGroupById(1L)).thenReturn(Optional.of(Group.builder()
+ .id(1L)
+ .title("Go to Goorm Store!")
+ .content("yummy yummy yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .currentParticipants(1)
+ .meetingDate(LocalDateTime.now())
+ .meetingStatus(MeetingStatus.OPEN)
+ .storeId(10L)
+ .build()));
+
+ // when
+ ResultActions actions = mockMvc.perform(get("/api/v1/groups/{groupId}", 1L)
+ .cookie(authCookie));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.msg").value("소모임 상세 정보 조회 성공"),
+ jsonPath("$.data.id").value(1L),
+ jsonPath("$.data.title").value("Go to Goorm Store!")
+ );
+
+ verify(groupService, times(1)).findGroupById(1L);
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("사용자는 소모임에 참여할 수 있다.")
+ void 소모임_가입_테스트() throws Exception {
+ // given
+ Long groupId = 1L;
+ Long storeId = 10L;
+
+ JoinGroupRequestDto joinRequestDto = JoinGroupRequestDto.builder()
+ .storeId(storeId)
+ .build();
+
+ doNothing().when(groupService).joinGroup(eq(groupId), any(), eq(storeId));
+
+ String body = objectMapper.writeValueAsString(joinRequestDto);
+
+ // when
+ ResultActions actions = mockMvc.perform(post("/api/v1/groups/{groupId}/join", groupId)
+ .cookie(authCookie)
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.msg").value("소모임 참여 성공")
+ );
+
+ verify(groupService, times(1)).joinGroup(eq(groupId), any(), eq(storeId));
+ }
+}
diff --git a/application/src/test/java/com/groom/yummy/controller/ReplyControllerTest.java b/application/src/test/java/com/groom/yummy/controller/ReplyControllerTest.java
new file mode 100644
index 0000000..7ed7954
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/controller/ReplyControllerTest.java
@@ -0,0 +1,193 @@
+package com.groom.yummy.controller;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.reply.Reply;
+import com.groom.yummy.reply.ReplyService;
+import com.groom.yummy.jwt.JwtProvider;
+import com.groom.yummy.reply.dto.ReplyRequestDto;
+import com.groom.yummy.reply.dto.ReplyUpdateRequestDto;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.List;
+
+import jakarta.servlet.http.Cookie;
+
+@WebMvcTest(ReplyController.class)
+class ReplyControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockitoBean
+ private ReplyService replyService;
+
+ @MockitoBean
+ private JwtProvider jwtProvider;
+
+ private Cookie authCookie;
+
+ @BeforeEach
+ void setUp() {
+ // Mock 사용자 설정
+ UserDetails principal = org.springframework.security.core.userdetails.User
+ .withUsername("testUser@gmail.com")
+ .password("password") // 비밀번호는 테스트용이므로 중요하지 않음
+ .roles("USER") // 역할 설정
+ .build();
+
+ // 인증 객체 생성
+ Authentication authentication = new UsernamePasswordAuthenticationToken(
+ principal, principal.getPassword(), principal.getAuthorities()
+ );
+
+ // SecurityContext에 인증 정보 설정
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ when(jwtProvider.validateToken("valid.jwt.token")).thenReturn(true);
+ when(jwtProvider.getUsername("valid.jwt.token")).thenReturn("testUser@gmail.com");
+
+ // 쿠키 객체 생성
+ authCookie = new Cookie("Authorization", "valid.jwt.token");
+ }
+
+
+ @Test
+ @DisplayName("사용자는 댓글을 등록할 수 있다.")
+ void registerReplyTest() throws Exception {
+ // given
+ Long groupId = 1L;
+ String content = "댓글 내용";
+ ReplyRequestDto requestDto = new ReplyRequestDto(content, null, 1L, groupId);
+ Reply savedReply = Reply.builder().id(1L).content(content).groupId(groupId).build();
+
+ // Mocking 서비스 호출
+ when(replyService.createReply(any(Reply.class))).thenReturn(savedReply);
+
+ String body = objectMapper.writeValueAsString(requestDto);
+
+ // when
+ ResultActions actions = mockMvc.perform(post("/api/v1/reply")
+ .cookie(authCookie) // Authorization 쿠키 설정
+ .with(csrf()) // CSRF 토큰 설정
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.id").value(savedReply.getId()),
+ jsonPath("$.data.content").value(savedReply.getContent()),
+ jsonPath("$.msg").value("댓글을 성공적으로 작성하였습니다.")
+ );
+
+ // verify 호출 횟수 검증
+ verify(replyService, times(1)).createReply(any(Reply.class));
+ }
+
+ @Test
+ @DisplayName("사용자는 특정 소모임의 댓글을 조회할 수 있다.")
+ void getAllRepliesTest() throws Exception {
+ // given
+ Long groupId = 1L;
+ Reply reply1 = Reply.builder().id(1L).content("댓글1").groupId(groupId).build();
+ Reply reply2 = Reply.builder().id(2L).content("댓글2").groupId(groupId).build();
+ Page replyPage = new PageImpl<>(List.of(reply1, reply2));
+
+ when(replyService.getAllReplies(eq(groupId), any())).thenReturn(replyPage);
+
+ // when
+ ResultActions actions = mockMvc.perform(get("/api/v1/reply/all")
+ .param("groupId", groupId.toString())
+ .param("page", "0")
+ .param("size", "10")
+ .cookie(authCookie)
+ .contentType(MediaType.APPLICATION_JSON));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.content").isArray(),
+ jsonPath("$.msg").value("댓글 조회 성공")
+ );
+
+ verify(replyService, times(1)).getAllReplies(eq(groupId), any());
+ }
+
+ @Test
+ @DisplayName("사용자는 댓글을 수정할 수 있다.")
+ void updateReplyTest() throws Exception {
+ // given
+ Long replyId = 1L;
+ String updatedContent = "수정된 댓글 내용";
+ ReplyUpdateRequestDto updateRequestDto = new ReplyUpdateRequestDto(updatedContent);
+ Reply updatedReply = Reply.builder().id(replyId).content(updatedContent).build();
+
+ when(replyService.updateReply(eq(replyId), eq(updatedContent))).thenReturn(updatedReply);
+
+ String body = objectMapper.writeValueAsString(updateRequestDto);
+
+ // when
+ ResultActions actions = mockMvc.perform(patch("/api/v1/reply/" + replyId)
+ .cookie(authCookie)
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(body));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.id").value(replyId),
+ jsonPath("$.data.content").value(updatedContent),
+ jsonPath("$.msg").value("댓글이 성공적으로 수정되었습니다.")
+ );
+
+ verify(replyService, times(1)).updateReply(eq(replyId), eq(updatedContent));
+ }
+
+ @Test
+ @DisplayName("사용자는 댓글을 삭제할 수 있다.")
+ void deleteReplyTest() throws Exception {
+ // given
+ Long replyId = 1L;
+
+ doNothing().when(replyService).deleteReply(replyId);
+
+ // when
+ ResultActions actions = mockMvc.perform(delete("/api/v1/reply/" + replyId)
+ .cookie(authCookie)
+ .with(csrf())
+ .contentType(MediaType.APPLICATION_JSON));
+
+ // then
+ actions.andExpectAll(
+ status().isOk(),
+ jsonPath("$.msg").value("댓글이 성공적으로 삭제되었습니다.")
+ );
+
+ verify(replyService, times(1)).deleteReply(replyId);
+ }
+}
diff --git a/application/src/test/java/com/groom/yummy/controller/UserControllerTest.java b/application/src/test/java/com/groom/yummy/controller/UserControllerTest.java
new file mode 100644
index 0000000..86e3b67
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/controller/UserControllerTest.java
@@ -0,0 +1,272 @@
+package com.groom.yummy.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.config.SecurityConfig;
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.JwtErrorCode;
+
+import com.groom.yummy.user.dto.request.UpdateNicknameReqDto;
+import com.groom.yummy.user.dto.response.UserInfoResDto;
+import com.groom.yummy.user.facade.UserFacade;
+import com.groom.yummy.jwt.JwtProvider;
+import com.groom.yummy.oauth2.handler.CustomSuccessHandler;
+import com.groom.yummy.oauth2.service.CustomOAuth2UserService;
+import jakarta.servlet.http.Cookie;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+@ActiveProfiles("test")
+@Import({SecurityConfig.class})
+@WebMvcTest(controllers = {UserController.class})
+class UserControllerTest {
+
+ @Autowired
+ protected MockMvc mockMvc;
+ @Autowired
+ protected ObjectMapper objectMapper;
+ @MockitoBean
+ protected JwtProvider jwtProvider;
+ @MockitoBean
+ protected UserFacade userFacade;
+ @MockitoBean
+ private CustomOAuth2UserService customOAuth2UserService;
+ @MockitoBean
+ private CustomSuccessHandler customSuccessHandler;
+
+ @BeforeEach
+ void setUp(){
+ String mockJwtToken = "mockJwtToken";
+ // JwtProvider Mock 설정
+ when(jwtProvider.validateToken(mockJwtToken)).thenReturn(false);
+ when(jwtProvider.getUserId(mockJwtToken)).thenReturn(1L);
+ when(jwtProvider.getUsername(mockJwtToken)).thenReturn("email@gmail.com");
+ when(jwtProvider.getName(mockJwtToken)).thenReturn("강형준");
+ when(jwtProvider.getRole(mockJwtToken)).thenReturn("ROLE_USER");
+ }
+
+ @Test
+ @DisplayName("토큰 기반 정보로 유저 조회 테스트")
+ void getUserInfoByTokenTest() throws Exception {
+ // given
+ Long userId = 1L;
+ String jwtToken = "mockJwtToken";
+ String email = "email@gmail.com";
+ String nickname = "강형준";
+ UserInfoResDto mockResponse = UserInfoResDto.builder().id(userId).email(email).nickname(nickname).build();
+ when(userFacade.getUserInfo(userId)).thenReturn(mockResponse);
+
+ // When & Then
+ ResultActions actions = mockMvc.perform(get("/api/v1/users")
+ .cookie(new Cookie("Authorization", jwtToken)) // 쿠키 추가
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.id").value(userId),
+ jsonPath("$.data.email").value(email),
+ jsonPath("$.data.nickname").value(nickname),
+ jsonPath("$.msg").value("자신의 정보 조회 성공")
+ );
+
+ verify(userFacade, times(1))
+ .getUserInfo(any(Long.class));
+ verify(jwtProvider, times(1))
+ .validateToken(any(String.class));
+ }
+
+ @Test
+ @DisplayName("토큰 검증 실패 - WRONG_TYPE_TOKEN 예외 테스트")
+ void getUserInfoWithWrongTypeTokenTest() throws Exception {
+ // given
+ String jwtToken = "mockJwtToken";
+ JwtErrorCode errorCode = JwtErrorCode.WRONG_TYPE_TOKEN;
+ doThrow(new CustomException(errorCode))
+ .when(jwtProvider).validateToken(any(String.class));
+
+ // When & Then
+ mockMvc.perform(get("/api/v1/users")
+ .cookie(new Cookie("Authorization", jwtToken))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnauthorized())
+ .andExpect(jsonPath("$.msg").value(errorCode.getMessage())) // 에러 메시지 검증
+ .andExpect(jsonPath("$.data").value(-1L));
+
+ verify(jwtProvider, times(1)).validateToken(any(String.class));
+ verify(userFacade, never()).getUserInfo(any(Long.class)); // 유저 조회는 호출되지 않아야 함
+ }
+
+ @Test
+ @DisplayName("토큰 검증 실패 - EXPIRED_TOKEN 예외 테스트")
+ void getUserInfoWithExpiredTokenTest() throws Exception {
+ // given
+ String jwtToken = "expiredJwtToken";
+ JwtErrorCode errorCode = JwtErrorCode.EXPIRED_TOKEN;
+ doThrow(new CustomException(errorCode))
+ .when(jwtProvider).validateToken(any(String.class));
+
+ // When & Then
+ mockMvc.perform(get("/api/v1/users")
+ .cookie(new Cookie("Authorization", jwtToken))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnauthorized())
+ .andExpect(jsonPath("$.msg").value(errorCode.getMessage()))
+ .andExpect(jsonPath("$.data").value(-1L));
+
+ verify(jwtProvider, times(1)).validateToken(any(String.class));
+ verify(userFacade, never()).getUserInfo(any(Long.class)); // 유저 조회는 호출되지 않아야 함
+ }
+
+ @Test
+ @DisplayName("토큰 검증 실패 - UNKNOWN_TOKEN_ERROR 예외 테스트")
+ void getUserInfoWithUnknownTokenErrorTest() throws Exception {
+ // given
+ String jwtToken = "unknownJwtToken";
+ JwtErrorCode errorCode = JwtErrorCode.UNKNOWN_TOKEN_ERROR;
+ doThrow(new CustomException(errorCode))
+ .when(jwtProvider).validateToken(any(String.class));
+
+ // When & Then
+ mockMvc.perform(get("/api/v1/users")
+ .cookie(new Cookie("Authorization", jwtToken))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.msg").value(errorCode.getMessage()))
+ .andExpect(jsonPath("$.data").value(-1L));
+
+ verify(jwtProvider, times(1)).validateToken(any(String.class));
+ verify(userFacade, never()).getUserInfo(any(Long.class)); // 유저 조회는 호출되지 않아야 함
+ }
+
+ @Test
+ @DisplayName("userId로 유저를 조회 테스트")
+ void getUserInfoTest() throws Exception{
+ // given
+ Long userId = 1L;
+ String jwtToken = "mockJwtToken";
+ String email = "email@gmail.com";
+ String nickname = "홍길동";
+ UserInfoResDto mockResponse = UserInfoResDto.builder().id(userId).email(email).nickname(nickname).build();
+ when(userFacade.getUserInfo(userId)).thenReturn(mockResponse);
+
+ // When & Then
+ ResultActions actions = mockMvc.perform(get("/api/v1/users/" + userId)
+ .cookie(new Cookie("Authorization", jwtToken)) // 쿠키 추가
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.id").value(userId),
+ jsonPath("$.data.email").value(email),
+ jsonPath("$.data.nickname").value(nickname),
+ jsonPath("$.msg").value("유저 정보 조회 성공")
+ );
+
+ verify(userFacade, times(1))
+ .getUserInfo(any(Long.class));
+ verify(jwtProvider, times(1))
+ .validateToken(any(String.class));
+ }
+
+ @Test
+ @DisplayName("유저 nickname 변경 테스트.")
+ void updateUserNicknameTest() throws Exception{
+ // given
+ Long userId = 1L;
+ String jwtToken = "mockJwtToken";
+ String email = "email@gmail.com";
+ String updateNick = "이후강형준";
+
+ UpdateNicknameReqDto updateNicknameReqDto = UpdateNicknameReqDto.builder().nickname(updateNick).build();
+ String body = objectMapper.writeValueAsString(updateNicknameReqDto);
+
+ UserInfoResDto mockResponse = UserInfoResDto.builder().id(userId).email(email).nickname(updateNick).build();
+ when(userFacade.updateUserNickname(userId, updateNicknameReqDto)).thenReturn(mockResponse);
+
+ // When & Then
+ ResultActions actions = mockMvc.perform(patch("/api/v1/users/profile")
+ .cookie(new Cookie("Authorization", jwtToken)) // 쿠키 추가
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpectAll(
+ status().isOk(),
+ jsonPath("$.data.id").value(userId),
+ jsonPath("$.data.email").value(email),
+ jsonPath("$.data.nickname").value(updateNick),
+ jsonPath("$.msg").value("유저 닉네임 변경 성공")
+ );
+
+ verify(userFacade, times(1))
+ .updateUserNickname(any(Long.class), any(UpdateNicknameReqDto.class));
+ verify(jwtProvider, times(1))
+ .validateToken(any(String.class));
+ }
+
+ @Test
+ @DisplayName("유저 닉네임은 공백일 수 없습니다.")
+ void updateUserNicknameTest_NonBlank() throws Exception{
+ // given
+ Long userId = 1L;
+ String jwtToken = "mockJwtToken";
+ String email = "email@gmail.com";
+ String updateNick = "";
+
+ UpdateNicknameReqDto updateNicknameReqDto = UpdateNicknameReqDto.builder().nickname(updateNick).build();
+ String body = objectMapper.writeValueAsString(updateNicknameReqDto);
+
+ // When & Then
+ ResultActions actions = mockMvc.perform(patch("/api/v1/users/profile")
+ .cookie(new Cookie("Authorization", jwtToken)) // 쿠키 추가
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpectAll(
+ status().isBadRequest(),
+ jsonPath("$.data").value(-1L),
+ jsonPath("$.msg").value("must not be blank")
+ );
+
+ verify(userFacade, never())
+ .updateUserNickname(any(Long.class), any(UpdateNicknameReqDto.class)); // 메서드가 호출되지 않아야 함
+ verify(jwtProvider, times(1))
+ .validateToken(any(String.class));
+ }
+
+ @Test
+ @DisplayName("유저 삭제(논리) 테스트")
+ void deleteUserByTokenTest() throws Exception{
+ // given
+ Long userId = 1L;
+ String jwtToken = "mockJwtToken";
+
+ when(userFacade.deleteUser(userId)).thenReturn(userId);
+
+ // When & Then
+ ResultActions actions = mockMvc.perform(delete("/api/v1/users")
+ .cookie(new Cookie("Authorization", jwtToken)) // 쿠키 추가
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpectAll(
+ status().isOk(),
+ jsonPath("$.data").value(userId),
+ jsonPath("$.msg").value("회원정보 삭제 성공")
+ );
+
+ verify(userFacade, times(1))
+ .deleteUser(any(Long.class));
+ verify(jwtProvider, times(1))
+ .validateToken(any(String.class));
+ }
+}
\ No newline at end of file
diff --git a/application/src/test/java/com/groom/yummy/oauth2/handler/CustomSuccessHandlerTest.java b/application/src/test/java/com/groom/yummy/oauth2/handler/CustomSuccessHandlerTest.java
new file mode 100644
index 0000000..cc407fa
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/oauth2/handler/CustomSuccessHandlerTest.java
@@ -0,0 +1,75 @@
+package com.groom.yummy.oauth2.handler;
+
+import com.groom.yummy.jwt.JwtProvider;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.user.User;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class CustomSuccessHandlerTest {
+
+ @InjectMocks
+ private CustomSuccessHandler customSuccessHandler;
+ @Mock
+ private JwtProvider jwtProvider;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp(){
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ jwtProvider.COOKIE_NAME = "Authorization";
+ jwtProvider.VALID_TIME = 1000L;
+ }
+
+ @Test
+ @DisplayName("OAuth2 성공 핸들러 테스트")
+ void onAuthenticationSuccessTest() throws ServletException, IOException {
+ // given
+ Long userId = 1L;
+ String email = "email@gmail.com";
+ String nickname = "강형준";
+ String role = "ROLE_USER";
+ String expectedToken = "mockAccessToken";
+ String cookieName = "Authorization";
+ Long validTime = 1000L;
+
+ User user = User.builder().id(userId).email(email).nickname(nickname).role(role).build();
+ LoginUser loginUser = new LoginUser(user);
+
+ Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null);
+
+ when(jwtProvider.createAccessToken(userId, email, nickname, role)).thenReturn(expectedToken);
+
+ // when
+ customSuccessHandler.onAuthenticationSuccess(request, response, authentication);
+
+ // then
+ Cookie cookie = response.getCookie(cookieName);
+ assertNotNull(cookie);
+ assertEquals(expectedToken, cookie.getValue());
+ assertEquals(validTime, cookie.getMaxAge());
+
+ verify(jwtProvider, times(1))
+ .createAccessToken(userId, email, nickname,role);
+ }
+}
\ No newline at end of file
diff --git a/application/src/test/java/com/groom/yummy/oauth2/service/CustomOAuth2UserServiceTest.java b/application/src/test/java/com/groom/yummy/oauth2/service/CustomOAuth2UserServiceTest.java
new file mode 100644
index 0000000..dcd92a8
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/oauth2/service/CustomOAuth2UserServiceTest.java
@@ -0,0 +1,140 @@
+package com.groom.yummy.oauth2.service;
+
+import com.groom.yummy.user.facade.UserFacade;
+import com.groom.yummy.oauth2.auth.LoginUser;
+import com.groom.yummy.oauth2.dto.OAuth2Response;
+import com.groom.yummy.oauth2.strategy.OAuth2ResFactory;
+import com.groom.yummy.user.User;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(MockitoExtension.class)
+class CustomOAuth2UserServiceTest {
+ @InjectMocks
+ private CustomOAuth2UserService customOAuth2UserService;
+
+ @Mock
+ private OAuth2ResFactory oAuth2ResFactory;
+
+ @Mock
+ private UserFacade userFacade;
+
+ @Mock
+ private DefaultOAuth2UserService defaultOAuth2UserService;
+
+ Map attributes;
+
+ @BeforeEach
+ void setUp(){
+ String registrationId = "kakao";
+ String email = "email@gmail.com";
+ String nickname = "강형준";
+
+ attributes = Map.of(
+ "id", registrationId,
+ "kakao_account", Map.of("email", email),
+ "properties", Map.of("nickname", nickname)
+ );
+ }
+
+ @Test
+ void kakaoOAuth2loadUserTest() throws Exception {
+ // given
+ String registrationId = "kakao";
+ String email = "email@gmail.com";
+ String nickname = "강형준";
+ String oauth2AccessToken = "testAccessToken";
+
+ // 가짜 웹 서버를 통해 실제 카카오 서버와의 통신을 mocking 합니다.
+ MockWebServer mockWebServer = new MockWebServer();
+ mockWebServer.start();
+
+ // 받은 엑세스 토큰을 통해, 클라이언트가 UserInfo Endpoint 요청 보내 사용자 정보를 받는 동작 Mocking
+ String userInfoResponse = "{ \"id\": \"" + registrationId +
+ "\", \"kakao_account\": { \"email\": \"" + email + "\" }," +
+ " \"properties\": { \"nickname\": \"" + nickname + "\" } }";
+ mockWebServer.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+ .setBody(userInfoResponse)
+ );
+
+ // Mock OAuth2AccessToken: 엑세스 토큰 Mocking
+ OAuth2AccessToken accessToken = new OAuth2AccessToken(
+ OAuth2AccessToken.TokenType.BEARER,
+ oauth2AccessToken,
+ Instant.now(),
+ Instant.now().plusSeconds(3600)
+ );
+
+ // OAuth2 인증 및 사용자 정보 요청 과정에서 필요한 클라이언트와 요청 정보를 Mocking
+ ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("kakao")
+ .clientId("test-client-id")
+ .clientSecret("test-client-secret")
+ .authorizationUri("https://kauth.kakao.com/oauth/authorize")
+ .tokenUri("https://kauth.kakao.com/oauth/token")
+ .userInfoUri(mockWebServer.url("/mock/url").toString())
+ .userNameAttributeName("id")
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
+ .build();
+ OAuth2UserRequest userRequest = new OAuth2UserRequest(clientRegistration, accessToken);
+
+ // 인증 이후 받은 사용자 정보 Mocking
+ OAuth2User oAuth2User = Mockito.mock(OAuth2User.class);
+ Mockito.when(oAuth2User.getAttributes()).thenReturn(Map.of(
+ "id", registrationId,
+ "kakao_account", Map.of("email", email),
+ "properties", Map.of("nickname", nickname)
+ ));
+
+ // 받은 사용자 정보를 변환하는 OAuth2Response 클래스 Mocking
+ OAuth2Response oAuth2Response = Mockito.mock(OAuth2Response.class);
+ Mockito.when(oAuth2Response.getEmail()).thenReturn(email);
+ Mockito.when(oAuth2Response.getName()).thenReturn(nickname);
+ Mockito.when(oAuth2ResFactory.createOAuth2Response(registrationId, oAuth2User.getAttributes()))
+ .thenReturn(oAuth2Response);
+
+ // userFacade 클래스 목킹
+ User mockUser = User.builder().email(email).nickname(nickname).build();
+ Mockito.when(userFacade.findAuthUserByEmail(email)).thenReturn(Optional.empty());
+ Mockito.when(userFacade.findOrCreateUser(Optional.empty(), nickname, email)).thenReturn(mockUser);
+
+ // when
+ OAuth2User result = customOAuth2UserService.loadUser(userRequest);
+
+ // then
+ assertNotNull(result);
+ assertTrue(result instanceof LoginUser); // 리턴 값이 LoginUser 객체인지 확ㅇㄴ
+
+ // OAuth2User 구현체인 LoginUser 로 캐스팅
+ LoginUser loginUser = (LoginUser) result;
+
+ assertEquals(email, loginUser.getUser().getEmail());
+ assertEquals(nickname, loginUser.getUser().getNickname());
+
+ // MockWebServer 종료
+ mockWebServer.shutdown();
+ }
+}
\ No newline at end of file
diff --git a/application/src/test/java/com/groom/yummy/oauth2/service/SomeApiServiceTest.java b/application/src/test/java/com/groom/yummy/oauth2/service/SomeApiServiceTest.java
new file mode 100755
index 0000000..1160e88
--- /dev/null
+++ b/application/src/test/java/com/groom/yummy/oauth2/service/SomeApiServiceTest.java
@@ -0,0 +1,127 @@
+package com.groom.yummy.oauth2.service;
+
+import com.groom.yummy.store.Store;
+import com.groom.yummy.store.StoreService;
+import com.groom.yummy.webclient.SomeApiService;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
+
+@SpringBootTest(
+ webEnvironment = SpringBootTest.WebEnvironment.MOCK
+)
+@ActiveProfiles("test")
+@Import(SomeApiServiceTest.TestConfig.class)
+class SomeApiServiceTest {
+
+ @Autowired
+ private SomeApiService someApiService;
+
+ @MockitoBean
+ private StoreService storeService;
+
+ private static MockWebServer mockWebServer;
+
+
+ @BeforeAll
+ static void setUpServer() throws Exception {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+ }
+
+ @AfterAll
+ static void tearDownServer() throws Exception {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ @DisplayName("특정 지역 요청 시 반환된 식당 정보 저장 테스트")
+ void 특정지역_식당정보_저장테스트() throws Exception {
+ // GIVEN
+ String mockResponseBody = """
+ {
+ "data": {
+ "stores": [
+ {
+ "storeId": 1,
+ "name": "맛집가게",
+ "regionId": 2,
+ "category": "001"
+ },
+ {
+ "storeId": 2,
+ "name": "맛집가게2",
+ "regionId": 2,
+ "category": "002"
+ }
+ ]
+ },
+ "message": "가게 목록이 성공적으로 조회되었습니다."
+ }
+ """;
+
+ mockWebServer.enqueue(new MockResponse()
+ .setBody(mockResponseBody)
+ .addHeader("Content-Type", "application/json"));
+
+ // WHEN
+ String regionCode = "1111000000";
+ someApiService.fetchStoresFromApi(regionCode);
+
+ // THEN
+ ArgumentCaptor storeCaptor = ArgumentCaptor.forClass(Store.class);
+ verify(storeService, times(2)).createStore(storeCaptor.capture());
+
+ List capturedStores = storeCaptor.getAllValues();
+
+ // 첫 번째 Store 검증
+ Store store1 = capturedStores.get(0);
+ assertEquals("맛집가게", store1.getName());
+ assertEquals("001", store1.getCategory().getApiCode());
+ assertEquals(2L, store1.getRegionId());
+
+ // 두 번째 Store 검증
+ Store store2 = capturedStores.get(1);
+ assertEquals("맛집가게2", store2.getName());
+ assertEquals("002", store2.getCategory().getApiCode());
+ assertEquals(2L, store2.getRegionId());
+
+ // Mock 서버 요청 검증
+ RecordedRequest recordedRequest = mockWebServer.takeRequest();
+ assertEquals("/api/v1/stores?regionCode=1111000000", recordedRequest.getPath());
+ assertEquals("GET", recordedRequest.getMethod());
+ }
+
+ @Configuration
+ static class TestConfig {
+ @Bean
+ public SomeApiService someApiService(StoreService storeService) {
+ String baseUrl = mockWebServer.url("/").toString();
+ return new SomeApiService(WebClient.builder().baseUrl(baseUrl), storeService);
+ }
+
+ @Bean
+ public StoreService storeService() {
+ return mock(StoreService.class);
+ }
+ }
+
+}
diff --git a/application/src/test/java/module/ControllerTestSupport.java b/application/src/test/java/module/ControllerTestSupport.java
new file mode 100644
index 0000000..4289ade
--- /dev/null
+++ b/application/src/test/java/module/ControllerTestSupport.java
@@ -0,0 +1,23 @@
+package module;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.groom.yummy.controller.UserController;
+import com.groom.yummy.jwt.JwtProvider;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+//@WebMvcTest(controllers = {UserController.class})
+//@ExtendWith(MockitoExtension.class) // Mockito 확장 사용
+//public class ControllerTestSupport {
+//
+// @Autowired
+// protected MockMvc mockMvc;
+// @Autowired
+// protected ObjectMapper objectMapper;
+// @Mock
+// protected JwtProvider jwtProvider;
+//}
diff --git a/build.gradle b/build.gradle
index 6a9117c..deff39f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,25 +1,84 @@
plugins {
- id 'java'
- id 'org.springframework.boot' version '3.4.1'
- id 'io.spring.dependency-management' version '1.1.7'
+ id 'java'
+ id 'org.springframework.boot' version '3.4.1' apply false
+ id 'io.spring.dependency-management' version '1.1.7' apply false
+ id 'jacoco'
}
-allprojects {
- repositories {
- mavenCentral()
- }
+tasks.named('test') {
+ useJUnitPlatform()
+ finalizedBy 'jacocoTestReport'
}
-dependencyManagement {
- imports {
- mavenBom "org.springframework.boot:spring-boot-dependencies:3.4.1"
- }
+allprojects {
+ group = 'com.groom.yummy'
+ version = '1.0.0' // 공통 버전 설정
+ repositories {
+ mavenCentral()
+ }
+ java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+ }
}
-bootJar {
- enabled = false
+subprojects {
+ apply plugin: 'java'
+ apply plugin: 'jacoco'
+
+ tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+ finalizedBy tasks.named("${project.name}JacocoTestReport")
+ }
+
+ tasks.register("${project.name}JacocoTestReport", JacocoReport) {
+ dependsOn tasks.named("test")
+ reports {
+ xml.required.set(true)
+ xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/${project.name}-jacoco.xml"))
+ }
+ classDirectories.setFrom(
+ fileTree(dir: layout.buildDirectory.dir("classes/java/main")).matching {
+ exclude(
+ "**/dto/**", // DTO 제외
+ "**/config/**", // Config 제외
+ "**/exception/**", // Exception 제외
+ "**/*Application*", // Main Application 제외
+ "**/event/**", // Event 제외
+ "**/response/**", // Response 관련 클래스 제외
+ "com/groom/yummy/domain/group/**", // group 도메인 제외
+ "com/groom/yummy/domain/reply/**" // reply 도메인 제외
+ )
+ }
+ )
+ }
}
-jar {
- enabled = false
+task jacocoRootReport(type: JacocoReport) {
+ dependsOn subprojects.test
+
+ reports {
+ xml.required = true
+ html.required = true
+ html.outputLocation = file("${buildDir}/reports/jacoco/html")
+ }
+
+ additionalSourceDirs.from files(subprojects.sourceSets.main.allSource.srcDirs)
+ sourceDirectories.from files(subprojects.sourceSets.main.allSource.srcDirs)
+ classDirectories.setFrom(
+ files(
+ subprojects.collect { subproject ->
+ fileTree(dir: "${subproject.buildDir}/classes/java/main").matching {
+ exclude(
+ "${subproject.name}/com/groom/yummy/domain/group/**", // group 도메인 제외
+ "${subproject.name}/com/groom/yummy/domain/reply/**" // reply 도메인 제외
+ )
+ }
+ }
+ )
+ )
+ executionData.from files(subprojects.jacocoTestReport.executionData)
}
+
+
diff --git a/common/build.gradle b/common/build.gradle
index df7ccaa..85ca075 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -1,5 +1,7 @@
plugins {
id 'java'
+ id 'org.springframework.boot' version '3.4.1'
+ id 'io.spring.dependency-management' version '1.1.7'
}
java {
@@ -8,5 +10,20 @@ java {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
-}
\ No newline at end of file
+ compileOnly 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
+
+}
+
+// 'bootJar' 태스크 비활성화
+bootJar {
+ enabled = false
+}
+
+// JAR 태스크 활성화
+//jar {
+// enabled = false
+//}
\ No newline at end of file
diff --git a/common/src/main/java/com/groom/yummy/dto/ResponseDto.java b/common/src/main/java/com/groom/yummy/dto/ResponseDto.java
new file mode 100644
index 0000000..15a7fdd
--- /dev/null
+++ b/common/src/main/java/com/groom/yummy/dto/ResponseDto.java
@@ -0,0 +1,15 @@
+package com.groom.yummy.dto;
+
+
+import lombok.Builder;
+import lombok.Getter;
+
+public record ResponseDto(T data, String msg) {
+ @Builder
+ public ResponseDto {
+ }
+
+ public static ResponseDto of(T data, String msg) {
+ return ResponseDto.builder().data(data).msg(msg).build();
+ }
+}
diff --git a/common/src/main/java/com/groom/yummy/exception/CustomException.java b/common/src/main/java/com/groom/yummy/exception/CustomException.java
new file mode 100644
index 0000000..bfad281
--- /dev/null
+++ b/common/src/main/java/com/groom/yummy/exception/CustomException.java
@@ -0,0 +1,12 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+
+@Getter
+public class CustomException extends RuntimeException{
+ private ErrorCode errorCode;
+ public CustomException(ErrorCode errorCode) {
+ super(errorCode.getMessage());
+ this.errorCode = errorCode;
+ }
+}
diff --git a/common/src/main/java/com/groom/yummy/exception/ErrorCode.java b/common/src/main/java/com/groom/yummy/exception/ErrorCode.java
new file mode 100644
index 0000000..0164c6b
--- /dev/null
+++ b/common/src/main/java/com/groom/yummy/exception/ErrorCode.java
@@ -0,0 +1,8 @@
+package com.groom.yummy.exception;
+
+import org.springframework.http.HttpStatusCode;
+
+public interface ErrorCode {
+ HttpStatusCode getCode();
+ String getMessage();
+}
diff --git a/domain/build.gradle b/domain/build.gradle
index 2a8af13..cc71f7a 100644
--- a/domain/build.gradle
+++ b/domain/build.gradle
@@ -10,19 +10,42 @@ version = 'unspecified'
dependencies {
+ compileOnly project(':common')
+ testImplementation project(':common')
+
+ // Swagger
+ compileOnly 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
+
+ // JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+
+ // Spring boot
implementation 'org.springframework.boot:spring-boot-starter-web'
+ // lombok
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
+ // JUnit
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
- implementation project(':storage')
- implementation project(':common')
+ // Mockito
+ testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0'
+
+ // AssertJ
+ testImplementation 'org.assertj:assertj-core:3.24.2'
+
}
test {
useJUnitPlatform()
-}
\ No newline at end of file
+}
+
+bootJar {
+ enabled = false
+}
+
+//jar {
+// enabled = false
+//}
diff --git a/domain/src/main/java/com/groom/yummy/exception/GroupErrorCode.java b/domain/src/main/java/com/groom/yummy/exception/GroupErrorCode.java
new file mode 100644
index 0000000..c2c2027
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/exception/GroupErrorCode.java
@@ -0,0 +1,19 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum GroupErrorCode implements ErrorCode{
+ GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "그룹이 존재하지 않습니다."),
+ GROUP_PARTICIPATION_FULL(HttpStatus.NOT_FOUND, "참가 인원이 가득 찼습니다."),
+ GROUP_CREATE_FAILED(HttpStatus.NOT_FOUND, "그룹 생성이 실패했습니다.")
+ ;
+ private final HttpStatus code;
+ private final String message;
+
+ GroupErrorCode(HttpStatus code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/exception/ReplyErrorCode.java b/domain/src/main/java/com/groom/yummy/exception/ReplyErrorCode.java
new file mode 100644
index 0000000..bd9bd62
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/exception/ReplyErrorCode.java
@@ -0,0 +1,17 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum ReplyErrorCode implements ErrorCode{
+ REPLY_NOT_FOUND(HttpStatus.NOT_FOUND, "댓글이 존재하지 않습니다."),
+ ;
+ private final HttpStatus code;
+ private final String message;
+
+ ReplyErrorCode(HttpStatus code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/exception/StoreErrorCode.java b/domain/src/main/java/com/groom/yummy/exception/StoreErrorCode.java
new file mode 100644
index 0000000..dd9403f
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/exception/StoreErrorCode.java
@@ -0,0 +1,17 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum StoreErrorCode implements ErrorCode{
+ STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "가게가 존재하지 않습니다."),
+ ;
+ private final HttpStatus code;
+ private final String message;
+
+ StoreErrorCode(HttpStatus code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/exception/UserErrorCode.java b/domain/src/main/java/com/groom/yummy/exception/UserErrorCode.java
new file mode 100644
index 0000000..aaa0124
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/exception/UserErrorCode.java
@@ -0,0 +1,18 @@
+package com.groom.yummy.exception;
+
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum UserErrorCode implements ErrorCode{
+ USER_NOT_FOUND(HttpStatus.NOT_FOUND, "유저가 존재하지 않습니다."),
+ ;
+
+ private final HttpStatus code;
+ private final String message;
+
+ UserErrorCode(HttpStatus code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/exception/handler/CustomExceptionHandler.java b/domain/src/main/java/com/groom/yummy/exception/handler/CustomExceptionHandler.java
new file mode 100644
index 0000000..987ccfc
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/exception/handler/CustomExceptionHandler.java
@@ -0,0 +1,29 @@
+package com.groom.yummy.exception.handler;
+
+import com.groom.yummy.dto.ResponseDto;
+import com.groom.yummy.exception.CustomException;
+import io.swagger.v3.oas.annotations.Hidden;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@Hidden
+@RestControllerAdvice
+public class CustomExceptionHandler {
+ @ExceptionHandler(CustomException.class)
+ public ResponseEntity> CustomException(CustomException ex){
+ log.error("커스텀 예외 발생 msg: {}", ex.getErrorCode().getMessage());
+ return ResponseEntity.status(ex.getErrorCode().getCode()).body(ResponseDto.of(-1,ex.getErrorCode().getMessage()));
+ }
+
+ // 기타예외 발생 시 500반환
+ @ExceptionHandler
+ public ResponseEntity handleException(Exception ex) {
+ String message = "서버 내부에 에러가 발생했습니다.";
+ log.error(message+":"+ex.getMessage()+ex.getStackTrace()+ex.getCause());
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ResponseDto(-1,ex.getMessage()));
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/group/Group.java b/domain/src/main/java/com/groom/yummy/group/Group.java
new file mode 100755
index 0000000..a9a1293
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/Group.java
@@ -0,0 +1,25 @@
+package com.groom.yummy.group;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Group {
+
+ private Long id;
+ private String title;
+ private String content;
+ private Integer maxParticipants;
+ private Integer minParticipants;
+ private Integer currentParticipants;
+ private LocalDateTime meetingDate;
+ private MeetingStatus meetingStatus;
+ private Long storeId;
+}
diff --git a/domain/src/main/java/com/groom/yummy/group/GroupRepository.java b/domain/src/main/java/com/groom/yummy/group/GroupRepository.java
new file mode 100755
index 0000000..cef353d
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/GroupRepository.java
@@ -0,0 +1,11 @@
+package com.groom.yummy.group;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface GroupRepository {
+ Optional findGroupById(Long id);
+ Long saveGroup(Group group);
+ List findAllGroups(String category, String regionCode, String storeName, int page);
+ void updateGroupParticipants(Long groupId, int newParticipantCount);
+}
diff --git a/domain/src/main/java/com/groom/yummy/group/GroupService.java b/domain/src/main/java/com/groom/yummy/group/GroupService.java
new file mode 100755
index 0000000..877a63f
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/GroupService.java
@@ -0,0 +1,85 @@
+package com.groom.yummy.group;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.GroupErrorCode;
+import com.groom.yummy.exception.StoreErrorCode;
+import com.groom.yummy.exception.UserErrorCode;
+import com.groom.yummy.store.Store;
+import com.groom.yummy.store.StoreRepository;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import com.groom.yummy.usertogroup.AttendanceStatus;
+import com.groom.yummy.usertogroup.UserToGroup;
+import com.groom.yummy.usertogroup.UserToGroupRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class GroupService {
+
+ private final GroupRepository groupRepository;
+ private final UserRepository userRepository;
+ private final UserToGroupRepository userToGroupRepository;
+ private final StoreRepository storeRepository;
+
+ public Long createGroup(Long storeId, Long userId, String title, String content, Integer maxParticipants, Integer minParticipants, LocalDateTime meetingDate) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
+
+ if (storeId == null) {
+ throw new CustomException(StoreErrorCode.STORE_NOT_FOUND);
+ }
+
+ Group group = Group.builder()
+ .title(title)
+ .content(content)
+ .maxParticipants(maxParticipants)
+ .minParticipants(minParticipants)
+ .currentParticipants(1)
+ .meetingDate(meetingDate)
+ .meetingStatus(MeetingStatus.OPEN)
+ .storeId(storeId)
+ .build();
+
+ try {
+ Long groupId = groupRepository.saveGroup(group);
+ userToGroupRepository.saveUserToGroup(groupId, user.getId(), true, AttendanceStatus.APPROVED);
+ return groupId;
+ } catch (Exception e) {
+ throw new CustomException(GroupErrorCode.GROUP_CREATE_FAILED);
+ }
+ }
+
+ public Optional findGroupById(Long id) {
+ return groupRepository.findGroupById(id);
+ }
+
+ public List getAllGroups(String category, String regionCode, String storeName, int page) {
+ return groupRepository.findAllGroups(category, regionCode, storeName, page);
+ }
+
+ public void joinGroup(Long groupId, Long userId, Long storeId) {
+ User user = userRepository.findById(userId)
+ .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
+
+ Group group = groupRepository.findGroupById(groupId)
+ .orElseThrow(() -> new CustomException(GroupErrorCode.GROUP_NOT_FOUND));
+
+// Store store = storeRepository.findStoreById(storeId)
+// .orElseThrow(() -> new CustomException(StoreErrorCode.STORE_NOT_FOUND));
+
+ if (group.getCurrentParticipants() >= group.getMaxParticipants()) {
+ throw new CustomException(GroupErrorCode.GROUP_PARTICIPATION_FULL);
+ }
+ groupRepository.updateGroupParticipants(groupId, group.getCurrentParticipants() + 1);
+
+ userToGroupRepository.saveUserToGroup(group.getId(), user.getId(), false, AttendanceStatus.APPROVED);
+ }
+
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/group/MeetingStatus.java b/domain/src/main/java/com/groom/yummy/group/MeetingStatus.java
new file mode 100755
index 0000000..4908403
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/MeetingStatus.java
@@ -0,0 +1,35 @@
+package com.groom.yummy.group;
+
+import lombok.Getter;
+
+@Getter
+public enum MeetingStatus {
+ OPEN("OPEN", "모집 중: 참가 요청을 받고 있는 상태"),
+ CLOSED("CLOSED", "모집 마감: 더 이상 참가 요청을 받지 않는 상태."),
+ FINALIZED("FINALIZED", "팀 결성 완료: 모집이 끝나고 팀이 확정된 상태"),
+ CANCELLED("CANCELLED", "모집 취소: 소모임 자체가 취소된 상태"),
+ IN_PROGRESS("IN_PROGRESS", "모임 진행 중: 팀 결성 후 모임이 진행 중인 상태"),
+ COMPLETED("COMPLETED", "모임 완료: 모임이 종료된 상태");
+
+ private final String code;
+ private final String description;
+
+ MeetingStatus(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (%s)", code, description);
+ }
+
+ public static MeetingStatus fromCode(String code) {
+ for (MeetingStatus status : MeetingStatus.values()) {
+ if (status.getCode().equalsIgnoreCase(code)) {
+ return status;
+ }
+ }
+ throw new IllegalArgumentException("Unknown MeetingStatus code: " + code);
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/request/CreateGroupRequestDto.java b/domain/src/main/java/com/groom/yummy/group/dto/request/CreateGroupRequestDto.java
new file mode 100755
index 0000000..d842b52
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/request/CreateGroupRequestDto.java
@@ -0,0 +1,32 @@
+package com.groom.yummy.group.dto.request;
+
+import com.groom.yummy.group.Group;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class CreateGroupRequestDto {
+ private Long storeId;
+ private String title;
+ private String content;
+ private Integer maxParticipants;
+ private Integer minParticipants;
+ private LocalDateTime meetingDate;
+
+ public Group toGroupDomain() {
+ return Group.builder()
+ .title(this.title)
+ .content(this.content)
+ .maxParticipants(this.maxParticipants)
+ .minParticipants(this.minParticipants)
+ .meetingDate(this.meetingDate)
+ .storeId(this.storeId)
+ .build();
+ }
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/request/JoinGroupRequestDto.java b/domain/src/main/java/com/groom/yummy/group/dto/request/JoinGroupRequestDto.java
new file mode 100755
index 0000000..142ad2b
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/request/JoinGroupRequestDto.java
@@ -0,0 +1,13 @@
+package com.groom.yummy.group.dto.request;
+
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class JoinGroupRequestDto {
+ private Long storeId;
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/response/GroupDetailResponseDto.java b/domain/src/main/java/com/groom/yummy/group/dto/response/GroupDetailResponseDto.java
new file mode 100755
index 0000000..4b746f1
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/response/GroupDetailResponseDto.java
@@ -0,0 +1,45 @@
+package com.groom.yummy.group.dto.response;
+
+import com.groom.yummy.group.Group;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class GroupDetailResponseDto{
+
+ private Long id;
+ private String title;
+ private String content;
+ private Integer maxParticipants;
+ private Integer minParticipants;
+ private Integer currentParticipants;
+ private LocalDateTime meetingDate;
+ private String meetingStatus;
+ private Long storeId;
+ private LocalDateTime createdAt;
+
+ private StoreDetailResponseDto storeDetailResponse;
+ private List participantResponseList;
+
+ public static GroupDetailResponseDto fromGroupDomain(Group group) {
+ return GroupDetailResponseDto.builder()
+ .id(group.getId())
+ .title(group.getTitle())
+ .content(group.getContent())
+ .maxParticipants(group.getMaxParticipants())
+ .minParticipants(group.getMinParticipants())
+ .currentParticipants(group.getCurrentParticipants())
+ .meetingDate(group.getMeetingDate())
+ .meetingStatus(group.getMeetingStatus().toString())
+ .storeId(group.getStoreId())
+ .createdAt(LocalDateTime.now())
+ .build();
+ }
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/response/GroupResponseDto.java b/domain/src/main/java/com/groom/yummy/group/dto/response/GroupResponseDto.java
new file mode 100755
index 0000000..95e34a3
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/response/GroupResponseDto.java
@@ -0,0 +1,40 @@
+package com.groom.yummy.group.dto.response;
+
+import com.groom.yummy.group.Group;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class GroupResponseDto {
+ private Long id;
+ private String title;
+ private String content;
+ private Integer maxParticipants;
+ private Integer minParticipants;
+ private Integer currentParticipants;
+ private LocalDateTime meetingDate;
+ private String meetingStatus;
+ private Long storeId;
+ private LocalDateTime createdAt;
+
+ public static GroupResponseDto fromGroupDomain(Group group) {
+ return GroupResponseDto.builder()
+ .id(group.getId())
+ .title(group.getTitle())
+ .content(group.getContent())
+ .maxParticipants(group.getMaxParticipants())
+ .minParticipants(group.getMinParticipants())
+ .currentParticipants(group.getCurrentParticipants())
+ .meetingDate(group.getMeetingDate())
+ .meetingStatus(group.getMeetingStatus().toString())
+ .storeId(group.getStoreId())
+ .createdAt(LocalDateTime.now())
+ .build();
+ }
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/response/ParticipantResponseDto.java b/domain/src/main/java/com/groom/yummy/group/dto/response/ParticipantResponseDto.java
new file mode 100755
index 0000000..02d57e0
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/response/ParticipantResponseDto.java
@@ -0,0 +1,15 @@
+package com.groom.yummy.group.dto.response;
+
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ParticipantResponseDto {
+ private Long userId;
+ private String nickname;
+ private String attendanceStatus;
+ private Boolean isLeader;
+}
diff --git a/domain/src/main/java/com/groom/yummy/group/dto/response/StoreDetailResponseDto.java b/domain/src/main/java/com/groom/yummy/group/dto/response/StoreDetailResponseDto.java
new file mode 100755
index 0000000..f65e126
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/group/dto/response/StoreDetailResponseDto.java
@@ -0,0 +1,16 @@
+package com.groom.yummy.group.dto.response;
+
+import lombok.*;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class StoreDetailResponseDto {
+ private Long id;
+ private String name;
+ private String category;
+ private Long regionId;
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/publisher/EventPublisher.java b/domain/src/main/java/com/groom/yummy/publisher/EventPublisher.java
new file mode 100644
index 0000000..7731c79
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/publisher/EventPublisher.java
@@ -0,0 +1,5 @@
+package com.groom.yummy.publisher;
+
+public interface EventPublisher {
+ void publish(Object event);
+}
diff --git a/domain/src/main/java/com/groom/yummy/publisher/SpringEventPublisher.java b/domain/src/main/java/com/groom/yummy/publisher/SpringEventPublisher.java
new file mode 100644
index 0000000..eb76ffa
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/publisher/SpringEventPublisher.java
@@ -0,0 +1,16 @@
+package com.groom.yummy.publisher;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class SpringEventPublisher implements EventPublisher{
+
+ private final ApplicationEventPublisher applicationEventPublisher;
+ @Override
+ public void publish(Object event) {
+ applicationEventPublisher.publishEvent(event);
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/region/Region.java b/domain/src/main/java/com/groom/yummy/region/Region.java
new file mode 100755
index 0000000..dab681e
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/region/Region.java
@@ -0,0 +1,17 @@
+package com.groom.yummy.region;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+public class Region {
+
+ private Long id;
+ private String regionName;
+ private String regionCode;
+}
diff --git a/domain/src/main/java/com/groom/yummy/region/RegionRepository.java b/domain/src/main/java/com/groom/yummy/region/RegionRepository.java
new file mode 100755
index 0000000..f5de7ca
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/region/RegionRepository.java
@@ -0,0 +1,10 @@
+package com.groom.yummy.region;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface RegionRepository {
+ Optional findRegionById(Long id);
+ Long saveRegion(Region region);
+ List findAllRegions();
+}
diff --git a/domain/src/main/java/com/groom/yummy/region/RegionService.java b/domain/src/main/java/com/groom/yummy/region/RegionService.java
new file mode 100755
index 0000000..341113b
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/region/RegionService.java
@@ -0,0 +1,34 @@
+package com.groom.yummy.region;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class RegionService {
+
+ private final RegionRepository regionRepository;
+
+ public Optional findRegionById(Long id) {
+ return regionRepository.findRegionById(id);
+ }
+
+ public Long createRegion(Region region) {
+ regionRepository.findAllRegions().stream()
+ .filter(r -> r.getRegionName().equals(region.getRegionName()) || r.getRegionCode().equals(region.getRegionCode()))
+ .findAny()
+ .ifPresent(r -> {
+ throw new IllegalArgumentException("이미 등록되어있는 지역입니다.");
+ });
+
+ return regionRepository.saveRegion(region);
+ }
+
+ public List getAllRegions() {
+ return regionRepository.findAllRegions();
+ }
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/reply/Reply.java b/domain/src/main/java/com/groom/yummy/reply/Reply.java
new file mode 100644
index 0000000..92b0eb1
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/reply/Reply.java
@@ -0,0 +1,35 @@
+package com.groom.yummy.reply;
+
+import java.time.LocalDateTime;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+public class Reply {
+ private Long id;
+ private String content;
+ private Long parentReplyId;
+ private Long userId;
+ private Long groupId;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+
+ @Builder
+ public Reply(Long id, String content, Long parentReplyId, Long userId, Long groupId, LocalDateTime createdAt,
+ LocalDateTime updatedAt) {
+ this.id = id;
+ this.content = content;
+ this.parentReplyId = parentReplyId;
+ this.userId = userId;
+ this.groupId = groupId;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ protected void updateReply(String content) {
+ this.content = content;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/groom/yummy/reply/ReplyRepository.java b/domain/src/main/java/com/groom/yummy/reply/ReplyRepository.java
new file mode 100644
index 0000000..efb1e3a
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/reply/ReplyRepository.java
@@ -0,0 +1,14 @@
+package com.groom.yummy.reply;
+
+import java.util.Optional;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface ReplyRepository {
+ Optional findById(Long id);
+ Optional save(Reply reply);
+ Page findAllByParentId(Long parentId, Pageable pageable);
+ Page findByGroupId(Long groupId, Pageable pageable);
+ void deleteById(Long id);
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/groom/yummy/reply/ReplyService.java b/domain/src/main/java/com/groom/yummy/reply/ReplyService.java
new file mode 100644
index 0000000..577056d
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/reply/ReplyService.java
@@ -0,0 +1,83 @@
+package com.groom.yummy.reply;
+
+import java.util.Optional;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.GroupErrorCode;
+import com.groom.yummy.exception.ReplyErrorCode;
+import com.groom.yummy.exception.UserErrorCode;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.groom.yummy.group.GroupRepository;
+import com.groom.yummy.publisher.EventPublisher;
+import com.groom.yummy.reply.event.ReplyUpdatedEvent;
+import com.groom.yummy.user.UserRepository;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class ReplyService {
+
+ private final ReplyRepository replyRepository;
+ private final UserRepository userRepository;
+ private final GroupRepository groupRepository;
+ private final EventPublisher eventPublisher;
+
+ @Transactional
+ public Reply createReply(Reply reply) {
+ validateUser(reply.getUserId());
+ validateGroup(reply.getGroupId());
+
+ Reply savedReply = replyRepository.save(reply)
+ .orElseThrow(() -> new CustomException(ReplyErrorCode.REPLY_NOT_FOUND));
+
+ return savedReply;
+ }
+
+ @Transactional
+ public Reply updateReply(Long id, String content) {
+ Reply reply = validateReply(id);
+
+ // TODO: 댓글 수정 권한
+
+ reply.updateReply(content);
+
+ eventPublisher.publish(new ReplyUpdatedEvent(reply.getId(), reply.getContent()));
+
+ return reply;
+ }
+
+ @Transactional(readOnly = true)
+ public Page getAllReplies(Long groupId, Pageable pageable) {
+ validateGroup(groupId);
+ return replyRepository.findByGroupId(groupId, pageable);
+ }
+
+ @Transactional
+ public void deleteReply(Long id) {
+ validateReply(id);
+ replyRepository.deleteById(id);
+ }
+
+ // 댓글 검증
+ private Reply validateReply(Long id) {
+ return replyRepository.findById(id)
+ .orElseThrow(() -> new CustomException(ReplyErrorCode.REPLY_NOT_FOUND));
+ }
+
+ // 사용자 검증
+ private void validateUser(Long userId) {
+ userRepository.findById(userId)
+ .orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
+ }
+
+ // 그룹 검증
+ private void validateGroup(Long groupId) {
+ groupRepository.findGroupById(groupId)
+ .orElseThrow(() -> new CustomException(GroupErrorCode.GROUP_NOT_FOUND));
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/reply/event/ReplyUpdatedEvent.java b/domain/src/main/java/com/groom/yummy/reply/event/ReplyUpdatedEvent.java
new file mode 100644
index 0000000..668c6be
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/reply/event/ReplyUpdatedEvent.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.reply.event;
+
+public record ReplyUpdatedEvent (
+ Long replyId,
+ String content
+) {}
diff --git a/domain/src/main/java/com/groom/yummy/store/Category_.java b/domain/src/main/java/com/groom/yummy/store/Category_.java
new file mode 100644
index 0000000..2ed09a3
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/store/Category_.java
@@ -0,0 +1,65 @@
+package com.groom.yummy.store;
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+public enum Category_ {
+ // TODO : apiCode 수정
+ CHICKEN("001","치킨"),
+ CHINESE("002","중식"),
+ CUTLET_SASHIMI("003","돈까스-회"),
+ PIZZA("004","피자"),
+ FAST_FOOD("005","패스트푸드"),
+ STEW_SOUP("006","찜-탕"),
+ JOKBAL_BOSSAM("007","족발-보쌈"),
+ SNACK("008","분식"),
+ CAFE_DESSERT("009","카페-디저트"),
+ KOREAN("010","한식"),
+ MEAT("011","고기"),
+ WESTERN("012","양식"),
+ ASIAN("013","아시안"),
+ LATE_NIGHT("014","야식"),
+ LUNCH_BOX("015","도시락");
+
+ private final String apiCode;
+ private final String description;
+
+ private static final Map API_CODE_TO_CATEGORY = new HashMap<>();
+
+ static {
+ for(Category_ category: values()){
+ API_CODE_TO_CATEGORY.put(category.getApiCode(), category);
+ }
+ }
+
+ Category_(String apiCode, String description) {
+ this.apiCode = apiCode;
+ this.description = description;
+ }
+
+ public static Category_ fromApiCode(String apiCode) {
+ return API_CODE_TO_CATEGORY.get(apiCode);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s (%s) ", apiCode, description);
+ }
+
+ public static boolean isValidCategory(Category_ category) {
+ return API_CODE_TO_CATEGORY.containsValue(category);
+ }
+
+ public static Category_ fromDescription(String description) {
+ for (Category_ category : Category_.values()) {
+ if (category.getDescription().equals(description)) {
+ return category;
+ }
+ }
+ throw new IllegalArgumentException("잘못된 카테고리: " + description);
+ }
+
+}
diff --git a/domain/src/main/java/com/groom/yummy/store/Store.java b/domain/src/main/java/com/groom/yummy/store/Store.java
new file mode 100755
index 0000000..f3dffaa
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/store/Store.java
@@ -0,0 +1,19 @@
+package com.groom.yummy.store;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Store {
+
+ private Long storeId;
+ private String name;
+ private Category_ category;
+ private Long regionId;
+
+}
diff --git a/domain/src/main/java/com/groom/yummy/store/StoreRepository.java b/domain/src/main/java/com/groom/yummy/store/StoreRepository.java
new file mode 100755
index 0000000..aae6f22
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/store/StoreRepository.java
@@ -0,0 +1,12 @@
+package com.groom.yummy.store;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface StoreRepository {
+ Optional findStoreById(Long id);
+ Long saveStore(Store store, Long regionId);
+ List findAllStores();
+ boolean existsByNameAndRegionId(String name, Long regionId);
+
+}
diff --git a/domain/src/main/java/com/groom/yummy/store/StoreService.java b/domain/src/main/java/com/groom/yummy/store/StoreService.java
new file mode 100755
index 0000000..a781a20
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/store/StoreService.java
@@ -0,0 +1,62 @@
+package com.groom.yummy.store;
+
+import com.groom.yummy.region.RegionService;
+import com.groom.yummy.store.dto.StoreApiResponseDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class StoreService {
+
+ private final StoreRepository storeRepository;
+ private final RegionService regionService;
+
+ public Long createStore(Store store) {
+ regionService.findRegionById(store.getRegionId())
+ .orElseThrow(() -> new IllegalArgumentException("지역을 찾을 수 없습니다."));
+
+ storeRepository.findAllStores().stream()
+ .filter(s -> s.getName().equals(store.getName()) && s.getRegionId().equals(store.getRegionId()))
+ .findAny()
+ .ifPresent(s -> {
+ throw new IllegalArgumentException("이미 등록되어있는 가게 정보 입니다.");
+ });
+
+ if (store.getCategory() == null || !Category_.isValidCategory(store.getCategory())) {
+ throw new IllegalArgumentException("등록되지 않은 카테고리 입니다.");
+ }
+
+ return storeRepository.saveStore(store, store.getRegionId());
+ }
+
+ public void saveStores(List storeDtos) {
+ List stores = storeDtos.stream()
+ .map(dto -> Store.builder()
+ .storeId(dto.getStoreId())
+ .name(dto.getName())
+ .category(Category_.fromApiCode(dto.getCategory()))
+ .regionId(dto.getRegionId())
+ .build())
+ .toList();
+
+ for (Store store : stores) {
+ boolean exists = storeRepository.existsByNameAndRegionId(store.getName(), store.getRegionId());
+ if (!exists) {
+ storeRepository.saveStore(store, store.getRegionId());
+ }
+ }
+ }
+
+ public Optional findStoreById(Long storeId) {
+ return storeRepository.findStoreById(storeId);
+ }
+
+ public List getAllStores() {
+ return storeRepository.findAllStores();
+ }
+}
+
diff --git a/domain/src/main/java/com/groom/yummy/store/dto/StoreApiResponseDto.java b/domain/src/main/java/com/groom/yummy/store/dto/StoreApiResponseDto.java
new file mode 100644
index 0000000..37f20f7
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/store/dto/StoreApiResponseDto.java
@@ -0,0 +1,27 @@
+package com.groom.yummy.store.dto;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.*;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StoreApiResponseDto {
+
+ private Long storeId;
+ private String name;
+ private String category;
+ private Long regionId;
+
+ public static StoreApiResponseDto fromJsonNode(JsonNode node) {
+ return StoreApiResponseDto.builder()
+ .storeId(node.path("storeId").asLong())
+ .name(node.path("name").asText())
+ .category(node.path("category").asText())
+ .regionId(node.path("regionId").asLong())
+ .build();
+ }
+
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/User.java b/domain/src/main/java/com/groom/yummy/user/User.java
new file mode 100644
index 0000000..1f04a19
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/User.java
@@ -0,0 +1,31 @@
+package com.groom.yummy.user;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+ private Long id;
+ private String nickname;
+ private String role;
+ private String email;
+ private Long groupJoinCount;
+ private Long groupAttendanceCount;
+ private boolean isDeleted;
+
+ protected void changeNickname(String nickname){
+ if (nickname == null || nickname.trim().isEmpty()) {
+ throw new RuntimeException("닉네임은 null이거나 빈 값일 수 없습니다.");
+ }
+ this.nickname = nickname;
+ }
+
+ protected void deleteUser(){
+ this.isDeleted = true;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/UserAuthService.java b/domain/src/main/java/com/groom/yummy/user/UserAuthService.java
new file mode 100644
index 0000000..02d366b
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/UserAuthService.java
@@ -0,0 +1,38 @@
+package com.groom.yummy.user;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class UserAuthService {
+ private final UserRepository userRepository;
+
+ public Optional findAuthUserByEmail(String email){
+ return userRepository.findByEmail(email);
+ }
+
+ @Transactional
+ public User findOrCreateUser(Optional optionalUser, String nickname, String email){
+ User user;
+ if(optionalUser.isEmpty()){
+ user = User.builder()
+ .nickname(nickname)
+ .email(email)
+ .role("ROLE_USER")
+ .groupJoinCount(0L)
+ .groupAttendanceCount(0L)
+ .isDeleted(false)
+ .build();
+ user = userRepository.save(user);
+ }else{
+ user = optionalUser.get();
+ }
+ return user;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/UserRepository.java b/domain/src/main/java/com/groom/yummy/user/UserRepository.java
new file mode 100644
index 0000000..ad84ac2
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/UserRepository.java
@@ -0,0 +1,9 @@
+package com.groom.yummy.user;
+
+import java.util.Optional;
+
+public interface UserRepository {
+ Optional findById(Long userId);
+ Optional findByEmail(String email);
+ User save(User user);
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/UserService.java b/domain/src/main/java/com/groom/yummy/user/UserService.java
new file mode 100644
index 0000000..cc8a078
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/UserService.java
@@ -0,0 +1,41 @@
+package com.groom.yummy.user;
+
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.UserErrorCode;
+import com.groom.yummy.publisher.EventPublisher;
+import com.groom.yummy.user.event.UserDeleteEvent;
+import com.groom.yummy.user.event.UserNicknameChangedEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class UserService {
+ private final UserRepository userRepository;
+ private final EventPublisher eventPublisher;
+
+ public User getUserInfo(Long userId) {
+ return userRepository.findById(userId).orElseThrow(()-> new CustomException(UserErrorCode.USER_NOT_FOUND));
+ }
+
+ @Transactional
+ public User updateNickname(Long userId, String nickname){
+ User user = userRepository.findById(userId).orElseThrow(()-> new CustomException(UserErrorCode.USER_NOT_FOUND));
+ user.changeNickname(nickname);
+ eventPublisher.publish(new UserNicknameChangedEvent(user.getId(),user.getNickname()));
+ return user;
+ }
+
+ @Transactional
+ public Long deleteUser(Long userId) {
+ User user = userRepository.findById(userId).orElseThrow(()-> new CustomException(UserErrorCode.USER_NOT_FOUND));
+ user.deleteUser();
+ eventPublisher.publish(new UserDeleteEvent(user.getId(),user.isDeleted()));
+ return user.getId();
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/dto/request/UpdateNicknameReqDto.java b/domain/src/main/java/com/groom/yummy/user/dto/request/UpdateNicknameReqDto.java
new file mode 100644
index 0000000..ff9592a
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/dto/request/UpdateNicknameReqDto.java
@@ -0,0 +1,15 @@
+package com.groom.yummy.user.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+
+import lombok.Builder;
+
+@Builder
+public record UpdateNicknameReqDto(
+
+ @Schema(description = "닉네임", example = "홍길동")
+ @NotBlank
+
+ String nickname
+){}
\ No newline at end of file
diff --git a/domain/src/main/java/com/groom/yummy/user/dto/response/UserInfoResDto.java b/domain/src/main/java/com/groom/yummy/user/dto/response/UserInfoResDto.java
new file mode 100644
index 0000000..901ab6d
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/dto/response/UserInfoResDto.java
@@ -0,0 +1,23 @@
+package com.groom.yummy.user.dto.response;
+
+import com.groom.yummy.user.User;
+import lombok.Builder;
+
+@Builder
+public record UserInfoResDto(
+ Long id,
+ String email,
+ String nickname,
+ Long joinCount,
+ Long participationCount
+) {
+ public static UserInfoResDto from(User user){
+ return UserInfoResDto.builder()
+ .id(user.getId())
+ .email(user.getEmail())
+ .nickname(user.getNickname())
+ .joinCount(user.getGroupJoinCount())
+ .participationCount(user.getGroupAttendanceCount())
+ .build();
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/user/event/UserDeleteEvent.java b/domain/src/main/java/com/groom/yummy/user/event/UserDeleteEvent.java
new file mode 100644
index 0000000..33febc4
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/event/UserDeleteEvent.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.user.event;
+
+public record UserDeleteEvent(
+ Long userId,
+ boolean isDeleted
+) {}
diff --git a/domain/src/main/java/com/groom/yummy/user/event/UserNicknameChangedEvent.java b/domain/src/main/java/com/groom/yummy/user/event/UserNicknameChangedEvent.java
new file mode 100644
index 0000000..c64b904
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/event/UserNicknameChangedEvent.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.user.event;
+
+public record UserNicknameChangedEvent(
+ Long userId,
+ String newNickname
+){}
diff --git a/domain/src/main/java/com/groom/yummy/user/facade/UserFacade.java b/domain/src/main/java/com/groom/yummy/user/facade/UserFacade.java
new file mode 100644
index 0000000..0944334
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/user/facade/UserFacade.java
@@ -0,0 +1,49 @@
+package com.groom.yummy.user.facade;
+
+import com.groom.yummy.user.dto.request.UpdateNicknameReqDto;
+import com.groom.yummy.user.dto.response.UserInfoResDto;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserAuthService;
+import com.groom.yummy.user.UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@Transactional(readOnly = true) // 서비스 계층과 충돌이 나지 않게 트랜잭션 선언
+public class UserFacade {
+
+ private final UserAuthService userAuthService;
+ private final UserService userService;
+
+ public UserInfoResDto getUserInfo(Long userId) {
+ User user = userService.getUserInfo(userId);
+ return UserInfoResDto.from(user);
+ }
+ public Optional findAuthUserByEmail(String email){
+ return userAuthService.findAuthUserByEmail(email);
+ }
+
+ @Transactional
+ public User findOrCreateUser(Optional optionalUser, String name, String email) {
+ return userAuthService.findOrCreateUser(optionalUser, name, email);
+ }
+
+ @Transactional
+ public UserInfoResDto updateUserNickname(Long id, UpdateNicknameReqDto updateNicknameReqDto){
+ String nickname = updateNicknameReqDto.nickname();
+ User user = userService.updateNickname(id, nickname);
+ return UserInfoResDto.from(user);
+ }
+
+ @Transactional
+ public Long deleteUser(Long userId) {
+ Long deleteUserId = userService.deleteUser(userId);
+ return deleteUserId;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/usertogroup/AttendanceStatus.java b/domain/src/main/java/com/groom/yummy/usertogroup/AttendanceStatus.java
new file mode 100644
index 0000000..43cc13b
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/usertogroup/AttendanceStatus.java
@@ -0,0 +1,17 @@
+package com.groom.yummy.usertogroup;
+
+public enum AttendanceStatus {
+ ATTENDED("참석"),
+ NO_SHOW("불참"),
+ APPROVED("승인됨");
+
+ private final String description;
+
+ AttendanceStatus(String description) {
+ this.description = description;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroup.java b/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroup.java
new file mode 100644
index 0000000..34f9bfd
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroup.java
@@ -0,0 +1,16 @@
+package com.groom.yummy.usertogroup;
+
+import com.groom.yummy.group.Group;
+import com.groom.yummy.user.User;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class UserToGroup {
+ private Long id;
+ private Group group;
+ private User user;
+ private boolean isLeader;
+ private AttendanceStatus attendanceStatus;
+}
diff --git a/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroupRepository.java b/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroupRepository.java
new file mode 100644
index 0000000..633e0c2
--- /dev/null
+++ b/domain/src/main/java/com/groom/yummy/usertogroup/UserToGroupRepository.java
@@ -0,0 +1,13 @@
+package com.groom.yummy.usertogroup;
+
+import com.groom.yummy.group.Group;
+import com.groom.yummy.user.User;
+
+import java.util.Optional;
+
+public interface UserToGroupRepository {
+ void saveUserToGroup(Long groupId, Long userId, boolean isLeader, AttendanceStatus attendanceStatus);
+ Optional findByGroupAndUser(Group group, User user);
+}
+
+
diff --git a/domain/src/test/java/com/groom/yummy/facade/UserFacadeTest.java b/domain/src/test/java/com/groom/yummy/facade/UserFacadeTest.java
new file mode 100644
index 0000000..0d5565c
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/facade/UserFacadeTest.java
@@ -0,0 +1,128 @@
+package com.groom.yummy.facade;
+
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserAuthService;
+import com.groom.yummy.user.UserService;
+import com.groom.yummy.user.dto.request.UpdateNicknameReqDto;
+import com.groom.yummy.user.dto.response.UserInfoResDto;
+
+import com.groom.yummy.user.facade.UserFacade;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class UserFacadeTest {
+ @InjectMocks
+ private UserFacade userFacade;
+ @Mock
+ private UserAuthService userAuthService;
+ @Mock
+ private UserService userService;
+
+ @Test
+ @DisplayName("유저 서비스에서 조회한 유저를 dto로 변환하여 리턴합니다.")
+
+ void getUserInfoTest() {
+ // given
+ Long userId = 1L;
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+
+ User mockUser = User.builder().id(userId).nickname(nickname).email(email).build();
+ when(userService.getUserInfo(userId)).thenReturn(mockUser);
+
+ // when
+ UserInfoResDto result = userFacade.getUserInfo(userId);
+
+ // then
+ assertNotNull(result);
+ assertEquals(mockUser.getEmail(), result.email());
+ assertEquals(mockUser.getNickname(), result.nickname());
+ }
+
+ @Test
+ @DisplayName("email로 조회한 User를 리턴합니다.")
+
+ void findAuthUserByEmailTest() {
+ // given
+ Long userId = 1L;
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+
+ User mockUser = User.builder().id(userId).nickname(nickname).email(email).build();
+ when(userAuthService.findAuthUserByEmail(email)).thenReturn(Optional.of(mockUser));
+
+ // when
+ Optional result = userFacade.findAuthUserByEmail(email);
+
+ // then
+ assertTrue(result.isPresent());
+ assertEquals(email, result.get().getEmail());
+ }
+
+ @Test
+ @DisplayName("유저 회원가입 결과를 받아 리턴합니다.")
+
+ void findOrCreateUserTest() {
+ // given
+ Long userId = 1L;
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+ Optional optionalUser = Optional.empty();
+ User mockUser = User.builder().id(userId).nickname(nickname).email(email).build();
+ when(userAuthService.findOrCreateUser(optionalUser, nickname, email)).thenReturn(mockUser);
+
+ // when
+ User result = userFacade.findOrCreateUser(optionalUser, nickname, email);
+
+ // then
+ assertNotNull(result);
+ assertEquals(email, result.getEmail());
+ assertEquals(nickname, result.getNickname());
+ }
+
+ @Test
+ @DisplayName("유저 업데이트한 결과를 dto로 변환하여 리턴합니다.")
+
+ void updateUserNicknameTest() {
+ // Arrange
+ Long userId = 1L;
+ String email = "email@gmail.com";
+ String newNickname = "업데이트강형준";
+ UpdateNicknameReqDto updateNicknameReqDto = new UpdateNicknameReqDto(newNickname);
+ User updateMockUser = User.builder().id(userId).nickname(newNickname).email(email).build();
+ when(userService.updateNickname(userId, newNickname)).thenReturn(updateMockUser);
+
+ // when
+ UserInfoResDto result = userFacade.updateUserNickname(userId, updateNicknameReqDto);
+
+ // then
+ assertNotNull(result);
+ assertEquals(newNickname, result.nickname());
+ }
+
+ @Test
+ @DisplayName("유저 삭제(논리) 후 삭제한 userID를 리턴합니다.")
+ void deleteUserTest() {
+
+ // given
+ Long userId = 1L;
+ when(userService.deleteUser(userId)).thenReturn(userId);
+
+ // when
+ Long result = userFacade.deleteUser(userId);
+
+ // then
+ assertNotNull(result);
+ assertEquals(userId, result);
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/groom/yummy/group/GroupServiceTest.java b/domain/src/test/java/com/groom/yummy/group/GroupServiceTest.java
new file mode 100755
index 0000000..ec97755
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/group/GroupServiceTest.java
@@ -0,0 +1,224 @@
+package com.groom.yummy.group;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.store.Store;
+import com.groom.yummy.store.StoreRepository;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import com.groom.yummy.usertogroup.AttendanceStatus;
+import com.groom.yummy.usertogroup.UserToGroupRepository;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class GroupServiceTest {
+
+ @InjectMocks
+ private GroupService groupService;
+
+ @Mock
+ private GroupRepository groupRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private UserToGroupRepository userToGroupRepository;
+
+ @Mock
+ private StoreRepository storeRepository;
+
+ @DisplayName("User can create a group.")
+ @Order(1)
+ @Test
+ void createGroupTest() {
+ // Given
+ Long storeId = 10L;
+ Long userId = 1L;
+ String title = "Go to Goorm store";
+ String content = "yummy yummy yummy yummy yummy";
+ Integer maxParticipants = 4;
+ Integer minParticipants = 3;
+ LocalDateTime meetingDate = LocalDateTime.now();
+
+ User user = User.builder()
+ .id(userId)
+ .email("Goorm@gmail.com")
+ .nickname("Goorm")
+ .role("USER")
+ .isDeleted(false)
+ .build();
+
+ when(userRepository.findById(userId))
+ .thenReturn(Optional.of(user));
+ when(groupRepository.saveGroup(any(Group.class)))
+ .thenReturn(1L);
+
+ // When
+ Long groupId = groupService.createGroup(storeId, userId, title, content, maxParticipants, minParticipants, meetingDate);
+
+ // Then
+ assertEquals(1L, groupId);
+ verify(groupRepository, times(1))
+ .saveGroup(any(Group.class));
+ verify(userToGroupRepository, times(1))
+ .saveUserToGroup(any(Long.class), any(Long.class), eq(true), eq(AttendanceStatus.APPROVED));
+ }
+
+
+ @DisplayName("User can view group details.")
+ @Order(2)
+ @Test
+ void viewGroupDetailsTest() {
+ // Given
+ Long groupId = 1L;
+ Group group = Group.builder()
+ .id(groupId)
+ .title("Go to Goorm store")
+ .content("yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .currentParticipants(2)
+ .storeId(10L)
+ .build();
+
+ when(groupRepository.findGroupById(groupId)).thenReturn(Optional.of(group));
+
+ // When
+ Optional foundGroup = groupService.findGroupById(groupId);
+
+ // Then
+ assertTrue(foundGroup.isPresent());
+ assertEquals(groupId, foundGroup.get().getId());
+ verify(groupRepository, times(1)).findGroupById(groupId);
+ }
+
+ @DisplayName("User can join a group.")
+ @Order(3)
+ @Test
+ void joinGroupTest() {
+ // Given
+ Long groupId = 1L;
+ Long userId = 1L;
+ Long storeId = 10L;
+
+ Group group = Group.builder()
+ .id(groupId)
+ .title("Go to Goorm store")
+ .content("yummy yummy")
+ .maxParticipants(4)
+ .minParticipants(3)
+ .currentParticipants(2)
+ .storeId(storeId)
+ .build();
+
+ User user = User.builder()
+ .id(userId)
+ .email("Goorm@gmail.com")
+ .nickname("Goorm")
+ .role("USER")
+ .isDeleted(false)
+ .build();
+
+ Store store = new Store(); // Store 객체 생성
+
+ when(userRepository.findById(userId)).thenReturn(Optional.of(user));
+ when(groupRepository.findGroupById(groupId)).thenReturn(Optional.of(group));
+
+ // Mock UserToGroupRepository 동작 설정
+ doNothing().when(userToGroupRepository)
+ .saveUserToGroup(eq(groupId), eq(userId), eq(false), eq(AttendanceStatus.APPROVED));
+
+ // When
+ groupService.joinGroup(groupId, userId, storeId);
+
+ // Then
+ verify(groupRepository, times(1)).updateGroupParticipants(eq(groupId), eq(3));
+ verify(userToGroupRepository, times(1))
+ .saveUserToGroup(eq(groupId), eq(userId), eq(false), eq(AttendanceStatus.APPROVED));
+ }
+
+ @DisplayName("Group join fails when participant limit exceeded.")
+ @Order(4)
+ @Test
+ void joinGroupParticipantLimitExceededTest() {
+ // Given
+ Long groupId = 1L;
+ Long userId = 1L;
+ Long storeId = 10L;
+
+ Group group = Group.builder()
+ .id(groupId)
+ .title("Go to Goorm store")
+ .content("yummy yummy")
+ .maxParticipants(4)
+ .currentParticipants(4) // 참여 인원 초과
+ .storeId(storeId)
+ .build();
+
+ User user = User.builder()
+ .id(userId)
+ .email("Goorm@gmail.com")
+ .nickname("Goorm")
+ .build();
+
+ Store store = new Store(); // Store 객체 생성
+
+ when(userRepository.findById(userId)).thenReturn(Optional.of(user));
+ when(groupRepository.findGroupById(groupId)).thenReturn(Optional.of(group));
+
+ // When
+ CustomException exception = assertThrows(CustomException.class,
+ () -> groupService.joinGroup(groupId, userId, storeId));
+
+ // Then
+ assertEquals("참가 인원이 가득 찼습니다.", exception.getMessage());
+ verify(groupRepository, never()).updateGroupParticipants(anyLong(), anyInt());
+ }
+
+
+ @DisplayName("Retrieve group list test.")
+ @Order(5)
+ @Test
+ void retrieveGroupListTest() {
+ // Given
+ String category = "KOREAN";
+ String regionCode = "SEOUL";
+ String storeName = "Goorm Store";
+ int page = 1;
+
+ List groups = List.of(
+ Group.builder()
+ .id(1L)
+ .title("Go to Goorm store")
+ .content("yummy yummy")
+ .maxParticipants(4)
+ .currentParticipants(2)
+ .storeId(10L)
+ .build()
+ );
+
+ when(groupRepository.findAllGroups(category, regionCode, storeName, page)).thenReturn(groups);
+
+ // When
+ List result = groupService.getAllGroups(category, regionCode, storeName, page);
+
+ // Then
+ assertEquals(1, result.size());
+ assertEquals("Go to Goorm store", result.get(0).getTitle());
+ verify(groupRepository, times(1)).findAllGroups(category, regionCode, storeName, page);
+ }
+}
diff --git a/domain/src/test/java/com/groom/yummy/region/RegionServiceTest.java b/domain/src/test/java/com/groom/yummy/region/RegionServiceTest.java
new file mode 100755
index 0000000..8924b6a
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/region/RegionServiceTest.java
@@ -0,0 +1,65 @@
+package com.groom.yummy.region;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class RegionServiceTest {
+
+ @Mock
+ private RegionRepository regionRepository;
+
+ @InjectMocks
+ private RegionService regionService;
+
+
+ @Test
+ @Order(1)
+ @DisplayName("지역 저장 성공 테스트")
+ void 지역_저장_성공_테스트() {
+ //Given
+ Region region = Region.builder()
+ .regionName("구름동")
+ .regionCode("99999999")
+ .build();
+
+ when(regionRepository.findAllRegions()).thenReturn(List.of());
+ when(regionRepository.saveRegion(any(Region.class))).thenReturn(1L);
+
+ // When
+ Long regionId = regionService.createRegion(region);
+
+ // Then
+ assertThat(regionId).isEqualTo(1L);
+ verify(regionRepository).saveRegion(any(Region.class));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("중복된 지역 정보로 저장 시 실패")
+ void 중복_자역_저장_실패_테스트() {
+ // Given
+ Region region = Region.builder()
+ .regionName("서울특별시 강남구")
+ .regionCode("1111010100")
+ .build();
+
+ when(regionRepository.findAllRegions()).thenReturn(List.of(region));
+
+ // When & Then
+ assertThrows(IllegalArgumentException.class, () -> regionService.createRegion(region));
+ verify(regionRepository, never()).saveRegion(any(Region.class));
+ }
+}
diff --git a/domain/src/test/java/com/groom/yummy/reply/ReplyServiceTest.java b/domain/src/test/java/com/groom/yummy/reply/ReplyServiceTest.java
new file mode 100644
index 0000000..2586322
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/reply/ReplyServiceTest.java
@@ -0,0 +1,172 @@
+package com.groom.yummy.reply;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.publisher.EventPublisher;
+import com.groom.yummy.reply.event.ReplyUpdatedEvent;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import com.groom.yummy.group.Group;
+import com.groom.yummy.group.GroupRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class ReplyServiceTest {
+
+ @InjectMocks
+ private ReplyService replyService;
+
+ @Mock
+ private ReplyRepository replyRepository;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private GroupRepository groupRepository;
+
+ @Mock
+ private EventPublisher eventPublisher;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ @DisplayName("사용자는 소모임에 댓그을 등록할 수 있다.")
+ void createReply_Success() {
+ // Given
+ Reply reply = Reply.builder().content("댓글 내용").userId(1L).groupId(1L).build();
+ Reply savedReply = Reply.builder().id(1L).content("댓글 내용").userId(1L).groupId(1L).build();
+
+ when(userRepository.findById(1L)).thenReturn(Optional.of(mock(User.class)));
+ when(groupRepository.findGroupById(1L)).thenReturn(Optional.of(mock(Group.class)));
+ when(replyRepository.save(reply)).thenReturn(Optional.of(savedReply));
+
+ // When
+ Reply result = replyService.createReply(reply);
+
+ // Then
+ assertNotNull(result);
+ assertEquals(savedReply.getId(), result.getId());
+ verify(replyRepository, times(1)).save(reply);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 사용자는 댓글을 등록할 수 없다.")
+ void createReply_UserNotFound() {
+ // Given
+ Reply reply = Reply.builder().content("댓글 내용").userId(1L).groupId(1L).build();
+
+ when(userRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // When & Then
+ CustomException exception = assertThrows(CustomException.class, () -> {
+ replyService.createReply(reply);
+ });
+
+ assertEquals("유저가 존재하지 않습니다.", exception.getMessage());
+ verify(replyRepository, never()).save(any());
+ }
+
+ @Test
+ @DisplayName("댓글을 수정할 수 있다.")
+ void updateReply_Success() {
+ // Given
+ Reply reply = Reply.builder().id(1L).content("기존 댓글 내용").build();
+ String updatedContent = "수정된 댓글 내용";
+
+ when(replyRepository.findById(1L)).thenReturn(Optional.of(reply));
+
+ // When
+ Reply result = replyService.updateReply(1L, updatedContent);
+
+ // Then
+ assertNotNull(result);
+ assertEquals(updatedContent, result.getContent());
+ verify(replyRepository, times(1)).findById(1L);
+ verify(eventPublisher, times(1)).publish(any(ReplyUpdatedEvent.class));
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글은 수정할 수 없다.")
+ void updateReply_ReplyNotFound() {
+ // Given
+ when(replyRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // When & Then
+ CustomException exception = assertThrows(CustomException.class, () -> {
+ replyService.updateReply(1L, "수정된 댓글 내용");
+ });
+
+ assertEquals("댓글이 존재하지 않습니다.", exception.getMessage());
+ verify(eventPublisher, never()).publish(any());
+ }
+
+ @Test
+ @DisplayName("소모임에 달리 모든 댓글을 조회할 수 있다.")
+ void getAllReplies_Success() {
+ // Given
+ Long groupId = 1L;
+ Pageable pageable = PageRequest.of(0, 10);
+ Reply reply1 = Reply.builder().id(1L).content("댓글 1").groupId(groupId).build();
+ Reply reply2 = Reply.builder().id(2L).content("댓글 2").groupId(groupId).build();
+ Page replyPage = new PageImpl<>(List.of(reply1, reply2));
+
+ when(groupRepository.findGroupById(groupId)).thenReturn(Optional.of(mock(Group.class)));
+ when(replyRepository.findByGroupId(eq(groupId), eq(pageable))).thenReturn(replyPage);
+
+ // When
+ Page result = replyService.getAllReplies(groupId, pageable);
+
+ // Then
+ assertNotNull(result);
+ assertEquals(2, result.getTotalElements());
+ verify(replyRepository, times(1)).findByGroupId(eq(groupId), eq(pageable));
+ }
+
+ @Test
+ @DisplayName("댓글을 삭제할 수 있다.")
+ void deleteReply_Success() {
+ // Given
+ Long replyId = 1L;
+ Reply reply = Reply.builder().id(replyId).content("댓글 내용").build();
+
+ when(replyRepository.findById(replyId)).thenReturn(Optional.of(reply));
+
+ // When
+ replyService.deleteReply(replyId);
+
+ // Then
+ verify(replyRepository, times(1)).deleteById(replyId);
+ }
+
+ @Test
+ @DisplayName("존재하지 않는 댓글은 삭제할 수 없다.")
+ void deleteReply_ReplyNotFound() {
+ // Given
+ when(replyRepository.findById(1L)).thenReturn(Optional.empty());
+
+ // When & Then
+ CustomException exception = assertThrows(CustomException.class, () -> {
+ replyService.deleteReply(1L);
+ });
+
+ assertEquals("댓글이 존재하지 않습니다.", exception.getMessage());
+ verify(replyRepository, never()).deleteById(any());
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/groom/yummy/store/StoreServiceTest.java b/domain/src/test/java/com/groom/yummy/store/StoreServiceTest.java
new file mode 100755
index 0000000..5389e33
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/store/StoreServiceTest.java
@@ -0,0 +1,77 @@
+package com.groom.yummy.store;
+
+import com.groom.yummy.region.Region;
+import com.groom.yummy.region.RegionService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class StoreServiceTest {
+
+ @Mock
+ private StoreRepository storeRepository;
+
+ @Mock
+ private RegionService regionService;
+
+ @InjectMocks
+ private StoreService storeService;
+
+
+ @Test
+ @Order(1)
+ @DisplayName("요청 API 가게 정보 등록 성공 테스트")
+ void 가게_등록_성공_테스트() {
+ // Given
+ Store store = Store.builder()
+ .name("GoormStore")
+ .category(Category_.PIZZA)
+ .regionId(1L)
+ .build();
+
+ Region region = Region.builder()
+ .id(1L)
+ .regionName("구름동")
+ .regionCode("99999999")
+ .build();
+
+ when(regionService.findRegionById(1L)).thenReturn(Optional.of(region));
+ when(storeRepository.saveStore(any(Store.class), any(Long.class))).thenReturn(1L);
+
+ // When
+ Long storeId = storeService.createStore(store);
+
+ // Then
+ assertThat(storeId).isEqualTo(1L);
+ verify(regionService).findRegionById(1L);
+ verify(storeRepository).saveStore(any(Store.class), eq(1L));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("요청 API 가게 정보 등록 실패 테스트")
+ void 가게_등록_실패_테스트() {
+ // GIVEN
+ Store store = Store.builder()
+ .name("GoormStore")
+ .category(Category_.PIZZA)
+ .regionId(1L)
+ .build();
+
+ // When & Then
+ assertThrows(IllegalArgumentException.class, () -> storeService.createStore(store));
+ verify(storeRepository, never()).saveStore(any(Store.class), anyLong());
+ }
+}
diff --git a/domain/src/test/java/com/groom/yummy/user/UserAuthServiceTest.java b/domain/src/test/java/com/groom/yummy/user/UserAuthServiceTest.java
new file mode 100644
index 0000000..5d90cc3
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/user/UserAuthServiceTest.java
@@ -0,0 +1,107 @@
+package com.groom.yummy.user;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class UserAuthServiceTest {
+ @InjectMocks
+ private UserAuthService userAuthService;
+ @Mock
+ private UserRepository userRepository;
+
+ @Test
+ @DisplayName("회원가입이 된 유저인지 확인하기 위해 email로 유저를 조회합니다.")
+ void findAuthUserByEmailTest() {
+ // given
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+ String role = "ROLE_USER";
+ User mockUser = User.builder()
+ .email(email)
+ .nickname(nickname)
+ .role(role)
+ .build();
+ when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser));
+
+ // when
+ Optional result = userAuthService.findAuthUserByEmail(email);
+
+ // then
+ assertTrue(result.isPresent());
+ assertEquals(email, result.get().getEmail());
+ assertEquals(nickname, result.get().getNickname());
+ }
+
+ @Test
+ @DisplayName("회원가입 하지 않은 유저를 조회합니다.")
+ void findAuthUserByEmailTest_Empty() {
+ // given
+ String email = "email@gmail.com";
+ when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
+
+ // when
+ Optional result = userAuthService.findAuthUserByEmail(email);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("유저가 없는 경우(Optional.empty()) 회원가입을 진행합니다.")
+ void findOrCreateUser_Create_Test() {
+ // given
+ Long userId = 1L;
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+ String role = "ROLE_USER";
+ Optional optionalUser = Optional.empty();
+
+ User createUser = User.builder().id(userId).email(email).nickname(nickname).role(role).build();
+ when(userRepository.save(any(User.class))).thenReturn(createUser);
+
+ // when
+ User result = userAuthService.findOrCreateUser(optionalUser, nickname, email);
+
+ // then
+ assertNotNull(result);
+ assertEquals(userId, result.getId());
+ assertEquals(email, result.getEmail());
+ assertEquals(nickname, result.getNickname());
+ assertEquals(role, result.getRole());
+ }
+
+ @Test
+ @DisplayName("유저가 있는 경우 해당 유저를 리턴합니다.")
+ void findOrCreateUser_Find_Test() {
+ // given
+ String nickname = "강형준";
+ String email = "email@gmail.com";
+ String role = "ROLE_USER";
+
+ User existingUser = User.builder()
+ .email(email)
+ .nickname(nickname)
+ .role(role)
+ .build();
+
+ Optional optionalUser = Optional.of(existingUser);
+
+ // when
+ User result = userAuthService.findOrCreateUser(optionalUser, nickname, email);
+
+ // then
+ assertNotNull(result);
+ assertEquals(existingUser, result);
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/groom/yummy/user/UserServiceTest.java b/domain/src/test/java/com/groom/yummy/user/UserServiceTest.java
new file mode 100644
index 0000000..17426ec
--- /dev/null
+++ b/domain/src/test/java/com/groom/yummy/user/UserServiceTest.java
@@ -0,0 +1,120 @@
+package com.groom.yummy.user;
+
+import com.groom.yummy.exception.CustomException;
+import com.groom.yummy.exception.UserErrorCode;
+import com.groom.yummy.publisher.EventPublisher;
+import com.groom.yummy.user.event.UserDeleteEvent;
+import com.groom.yummy.user.event.UserNicknameChangedEvent;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class UserServiceTest {
+ @InjectMocks
+ private UserService userService;
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private EventPublisher eventPublisher;
+
+ @Test
+ @DisplayName("유저를 조회합니다.")
+ void getUserInfoTest() {
+ // given
+ Long userId = 1L;
+ String email = "email@gmail.com";
+ String nickname = "깅형준";
+ User mockUser = User.builder().id(userId).email(email).nickname(nickname).build();
+ when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
+
+ // when
+ User result = userService.getUserInfo(userId);
+
+ // then
+ assertNotNull(result);
+ assertEquals(userId, result.getId());
+ assertEquals(email, result.getEmail());
+ assertEquals(nickname, result.getNickname());
+ verify(userRepository, times(1)).findById(userId);
+ }
+
+ @Test
+ @DisplayName("유저 조회 결과가 Optional.empty 이면 예외가 발생합니다.")
+ void getUserInfoFailTest() {
+ // when
+ Long userId = 1L;
+ when(userRepository.findById(userId)).thenReturn(Optional.empty());
+
+ // when&then
+ CustomException exception = assertThrows(CustomException.class, () -> userService.getUserInfo(userId));
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getErrorCode());
+ verify(userRepository, times(1)).findById(userId);
+ }
+
+ @Test
+ @DisplayName("유저의 닉네임을 변경합니다.")
+ void updateNicknameTest() {
+ // given
+ Long userId = 1L;
+ String email = "email@gmail.com";
+ String oldNickname = "강형준";
+ String newNickname = "newNickname";
+ User mockUser = spy(User.builder().id(userId).email(email).nickname(oldNickname).build());
+ when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
+
+ // when
+ User result = userService.updateNickname(userId, newNickname);
+
+ // then
+ assertNotNull(result);
+ assertEquals(newNickname, result.getNickname());
+ verify(mockUser, times(1)).changeNickname(newNickname);
+ verify(eventPublisher, times(1)).publish(any(UserNicknameChangedEvent.class));
+ }
+
+ @Test
+ @DisplayName("유저를 삭제(논리) 합니다.")
+ void deleteUserTest() {
+ // given
+ Long userId = 1L;
+ String email = "email@gmail.com";
+ String nickname = "강형준";
+
+ User mockUser = spy(User.builder().id(userId).email(email).nickname(nickname).build());
+ when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
+
+ // when
+ Long result = userService.deleteUser(userId);
+
+ // then
+ assertNotNull(result);
+ assertEquals(userId, result);
+ verify(mockUser, times(1)).deleteUser();
+ verify(eventPublisher, times(1)).publish(any(UserDeleteEvent.class));
+ }
+
+ @Test
+ @DisplayName("없는 유저를 삭제하면 예외가 발생합니다.")
+ void deleteUserFailTest() {
+ // given
+ Long userId = 1L;
+ when(userRepository.findById(userId)).thenReturn(Optional.empty());
+
+ // when&then
+ CustomException exception = assertThrows(CustomException.class, () -> userService.deleteUser(userId));
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getErrorCode());
+ verify(userRepository, times(1)).findById(userId);
+ }
+
+}
diff --git a/settings.gradle b/settings.gradle
index 06650c3..7f89c0b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,10 +1,2 @@
rootProject.name = 'famous_restaurant'
-include 'group'
-include 'region'
-include 'store'
-include 'application'
-include 'storage'
-include 'user'
-include 'common'
-include 'domain'
-
+include 'application', 'storage', 'common', 'domain'
diff --git a/storage/build.gradle b/storage/build.gradle
index e711040..a00aaf3 100644
--- a/storage/build.gradle
+++ b/storage/build.gradle
@@ -4,23 +4,40 @@ plugins {
id 'io.spring.dependency-management' version '1.1.7'
}
-group = 'com.goorm'
-version = 'unspecified'
-
-java {
- toolchain {
- languageVersion = JavaLanguageVersion.of(17)
- }
-}
+version = '1.0.0'
dependencies {
+ // 모듈 간 의존성
+ implementation project(':domain')
+ compileOnly project(':common')
+ testImplementation project(':common')
- implementation 'org.projectlombok:lombok'
- annotationProcessor 'org.projectlombok:lombok'
-
+ // Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ // Database
runtimeOnly 'com.mysql:mysql-connector-j'
+ runtimeOnly 'com.h2database:h2'
+
+ // Lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+
+ // Testing
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation platform('org.junit:junit-bom:5.10.0')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+}
+bootJar {
+ enabled = false
}
+//jar {
+// enabled = false
+//}
+
+test {
+ useJUnitPlatform()
+}
diff --git a/storage/src/main/java/com/groom/yummy/config/H2Config.java b/storage/src/main/java/com/groom/yummy/config/H2Config.java
new file mode 100644
index 0000000..882f766
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/config/H2Config.java
@@ -0,0 +1,58 @@
+//package com.groom.yummy.config;
+//
+//import jakarta.persistence.EntityManagerFactory;
+//import org.springframework.beans.factory.annotation.Qualifier;
+//import org.springframework.boot.jdbc.DataSourceBuilder;
+//import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.context.annotation.Primary;
+//import org.springframework.context.annotation.Profile;
+//import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+//import org.springframework.orm.jpa.JpaTransactionManager;
+//import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+//import org.springframework.transaction.PlatformTransactionManager;
+//import org.springframework.transaction.annotation.EnableTransactionManagement;
+//
+//import javax.sql.DataSource;
+//import java.util.HashMap;
+//import java.util.Map;
+//
+//@Configuration
+//@Profile("test")
+//@EnableTransactionManagement
+//@EnableJpaRepositories(basePackages = {"com.groom.yummy.domain"})
+//public class H2Config {
+// @Bean(name = "dataSource")
+// @Primary
+// public DataSource h2DataSource() {
+// return DataSourceBuilder.create()
+// .driverClassName("org.h2.Driver")
+// .url("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
+// .username("sa")
+// .password("")
+// .build();
+// }
+//
+// @Primary
+// @Bean(name = "entityManagerFactory")
+// public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
+// @Qualifier("dataSource") DataSource dataSource) {
+// Map properties = new HashMap<>();
+// properties.put("hibernate.hbm2ddl.auto", "create");
+// properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
+//
+// return builder
+// .dataSource(dataSource)
+// .packages("com.groom.yummy.domain")
+// .persistenceUnit("test")
+// .properties(properties)
+// .build();
+// }
+//
+// @Primary
+// @Bean(name = "transactionManager")
+// public PlatformTransactionManager transactionManager(@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
+// return new JpaTransactionManager(entityManagerFactory);
+// }
+//}
diff --git a/storage/src/main/java/com/groom/yummy/config/MySqlConfig.java b/storage/src/main/java/com/groom/yummy/config/MySqlConfig.java
new file mode 100644
index 0000000..b146c76
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/config/MySqlConfig.java
@@ -0,0 +1,53 @@
+//package com.groom.yummy.config;
+//
+//
+//import com.zaxxer.hikari.HikariDataSource;
+//import jakarta.persistence.EntityManagerFactory;
+//import org.springframework.beans.factory.annotation.Qualifier;
+//import org.springframework.boot.context.properties.ConfigurationProperties;
+//import org.springframework.boot.jdbc.DataSourceBuilder;
+//import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.context.annotation.Primary;
+//import org.springframework.context.annotation.Profile;
+//import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+//import org.springframework.orm.jpa.JpaTransactionManager;
+//import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+//import org.springframework.transaction.PlatformTransactionManager;
+//import org.springframework.transaction.annotation.EnableTransactionManagement;
+//
+//
+//import javax.sql.DataSource;
+//import java.util.HashMap;
+//import java.util.Map;
+//
+//@Configuration
+//@Profile("!test")
+//@EnableTransactionManagement
+//@EnableJpaRepositories(basePackages = {"com.groom.yummy.domain"})
+//public class MySqlConfig {
+// @Bean(name = "dataSource")
+// @Primary
+// @ConfigurationProperties(prefix = "spring.datasource")
+// public DataSource dataSource(){
+// return DataSourceBuilder.create().type(HikariDataSource.class).build();
+// }
+// @Primary
+// @Bean(name = "entityManagerFactory")
+// public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) {
+//
+// Map properties = new HashMap();
+// properties.put("hibernate.hbm2ddl.auto", "update");
+// properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
+//
+// return builder.dataSource(dataSource).packages("com.groom.yummy.domain").persistenceUnit("primary").properties(properties).build();
+// }
+//
+// @Primary
+// @Bean(name = "transactionManager")
+// PlatformTransactionManager transactionManager(@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
+// return new JpaTransactionManager(entityManagerFactory);
+// }
+//}
+//
diff --git a/storage/src/main/java/com/groom/yummy/domain/BaseEntity.java b/storage/src/main/java/com/groom/yummy/domain/BaseEntity.java
new file mode 100644
index 0000000..d7dd69e
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/BaseEntity.java
@@ -0,0 +1,31 @@
+package com.groom.yummy.domain;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+@SuperBuilder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EntityListeners(AuditingEntityListener.class)
+@Getter
+public class BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @CreationTimestamp
+ private LocalDateTime createdAt;
+
+ @UpdateTimestamp
+ private LocalDateTime updatedAt;
+
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/group/GroupEntity.java b/storage/src/main/java/com/groom/yummy/domain/group/GroupEntity.java
new file mode 100644
index 0000000..82be750
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/group/GroupEntity.java
@@ -0,0 +1,90 @@
+package com.groom.yummy.domain.group;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.domain.store.StoreEntity;
+import com.groom.yummy.group.Group;
+import com.groom.yummy.group.MeetingStatus;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import java.time.LocalDateTime;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Table(name = "teams")
+@SuperBuilder
+public class GroupEntity extends BaseEntity {
+
+ @Column(nullable = false)
+ private String title;
+
+ @Column(nullable = false)
+ private String content;
+
+ @Column(nullable = false)
+ private Integer maxParticipants;
+
+ @Column(nullable = false)
+ private Integer minParticipants;
+
+ @Column(nullable = false)
+ private Integer currentParticipants;
+
+ @Column(nullable = false)
+ private LocalDateTime meetingDate;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private MeetingStatus meetingStatus;
+
+ private Long storeId;
+
+ @Builder
+ public GroupEntity(String title, String content, Integer maxParticipants, Integer minParticipants,
+ Integer currentParticipants, LocalDateTime meetingDate, MeetingStatus meetingStatus, Long storeId) {
+ this.title = title;
+ this.content = content;
+ this.maxParticipants = maxParticipants;
+ this.minParticipants = minParticipants;
+ this.currentParticipants = currentParticipants;
+ this.meetingDate = meetingDate;
+ this.meetingStatus = meetingStatus;
+ this.storeId = storeId;
+ }
+
+ public void updateCurrentCount(int count){
+ this.currentParticipants = count;
+ }
+
+ public static Group toGroupDomain(GroupEntity groupEntity) {
+ return Group.builder()
+ .id(groupEntity.getId())
+ .title(groupEntity.getTitle())
+ .content(groupEntity.getContent())
+ .maxParticipants(groupEntity.getMaxParticipants())
+ .minParticipants(groupEntity.getMinParticipants())
+ .currentParticipants(groupEntity.getCurrentParticipants())
+ .meetingDate(groupEntity.getMeetingDate())
+ .meetingStatus(groupEntity.getMeetingStatus())
+ .storeId(groupEntity.getStoreId())
+ .build();
+ }
+
+ public static GroupEntity fromGroupDomain(Group group) {
+ return GroupEntity.builder()
+ .title(group.getTitle())
+ .content(group.getContent())
+ .maxParticipants(group.getMaxParticipants())
+ .minParticipants(group.getMinParticipants())
+ .currentParticipants(group.getCurrentParticipants())
+ .meetingDate(group.getMeetingDate())
+ .meetingStatus(group.getMeetingStatus())
+ .storeId(group.getStoreId())
+ .build();
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/group/GroupEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/group/GroupEntityRepository.java
new file mode 100644
index 0000000..f218b5f
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/group/GroupEntityRepository.java
@@ -0,0 +1,58 @@
+package com.groom.yummy.domain.group;
+
+import com.groom.yummy.domain.store.StoreEntity;
+import com.groom.yummy.domain.store.StoreJpaRepository;
+import com.groom.yummy.group.Group;
+import com.groom.yummy.group.GroupRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+
+@Repository
+@RequiredArgsConstructor
+public class GroupEntityRepository implements GroupRepository {
+
+ private final GroupJpaRepository groupJpaRepository;
+
+ @Override
+ public Long saveGroup(Group group) {
+// StoreEntity storeEntity = storeJpaRepository.findById(storeId)
+// .orElseThrow(() -> new IllegalArgumentException("Store not found for id: " + storeId));
+ GroupEntity groupEntity = GroupEntity.fromGroupDomain(group);
+ return groupJpaRepository.save(groupEntity).getId();
+ }
+
+ @Override
+ public Optional findGroupById(Long id) {
+ return groupJpaRepository.findById(id).map(GroupEntity::toGroupDomain);
+ }
+
+ @Override
+ public List findAllGroups(String category, String regionCode, String storeName, int page) {
+ return groupJpaRepository.findAll().stream()
+ .map(GroupEntity::toGroupDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void updateGroupParticipants(Long groupId, int participants) {
+ GroupEntity groupEntity = groupJpaRepository.findById(groupId)
+ .orElseThrow(() -> new IllegalArgumentException("Group not found for id: " + groupId));
+// groupEntity = GroupEntity.builder()
+// .title(groupEntity.getTitle())
+// .content(groupEntity.getContent())
+// .maxParticipants(groupEntity.getMaxParticipants())
+// .minParticipants(groupEntity.getMinParticipants())
+// .currentParticipants(participants)
+// .meetingDate(groupEntity.getMeetingDate())
+// .meetingStatus(groupEntity.getMeetingStatus())
+// .storeId(groupEntity.getStoreId())
+// .build();
+// groupJpaRepository.save(groupEntity);
+ groupEntity.updateCurrentCount(participants);
+ }
+}
\ No newline at end of file
diff --git a/storage/src/main/java/com/groom/yummy/domain/group/GroupJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/group/GroupJpaRepository.java
new file mode 100644
index 0000000..f19cf3e
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/group/GroupJpaRepository.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.domain.group;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface GroupJpaRepository extends JpaRepository {
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/region/RegionEntity.java b/storage/src/main/java/com/groom/yummy/domain/region/RegionEntity.java
new file mode 100644
index 0000000..f19cb27
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/region/RegionEntity.java
@@ -0,0 +1,45 @@
+package com.groom.yummy.domain.region;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.region.Region;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Table(name = "region")
+@SuperBuilder
+public class RegionEntity extends BaseEntity {
+
+ @Column(nullable = false, unique = true)
+ private String regionName;
+
+ @Column(nullable = false, unique = true)
+ private String regionCode;
+
+ @Builder
+ public RegionEntity(String regionName, String regionCode) {
+ this.regionName = regionName;
+ this.regionCode = regionCode;
+ }
+
+ public Region toRegionDomain() {
+ return Region.builder()
+ .id(this.getId())
+ .regionName(this.regionName)
+ .regionCode(this.regionCode)
+ .build();
+ }
+
+ public static RegionEntity fromRegionDomain(Region region) {
+ if (region == null) {
+ throw new IllegalArgumentException("지역 설정은 필수입니다.");
+ }
+ return new RegionEntity(region.getRegionName(), region.getRegionCode());
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/region/RegionEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/region/RegionEntityRepository.java
new file mode 100755
index 0000000..3b27723
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/region/RegionEntityRepository.java
@@ -0,0 +1,35 @@
+package com.groom.yummy.domain.region;
+
+import com.groom.yummy.region.Region;
+import com.groom.yummy.region.RegionRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Repository
+@RequiredArgsConstructor
+public class RegionEntityRepository implements RegionRepository {
+
+ private final RegionJpaRepository regionJpaRepository;
+
+ @Override
+ public Optional findRegionById(Long id) {
+ return regionJpaRepository.findById(id).map(RegionEntity::toRegionDomain);
+ }
+
+ @Override
+ public Long saveRegion(Region region) {
+ RegionEntity regionEntity = RegionEntity.fromRegionDomain(region);
+ return regionJpaRepository.save(regionEntity).getId();
+ }
+
+ @Override
+ public List findAllRegions() {
+ return regionJpaRepository.findAll().stream()
+ .map(RegionEntity::toRegionDomain)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/region/RegionJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/region/RegionJpaRepository.java
new file mode 100644
index 0000000..aa0a374
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/region/RegionJpaRepository.java
@@ -0,0 +1,6 @@
+package com.groom.yummy.domain.region;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface RegionJpaRepository extends JpaRepository {
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntity.java b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntity.java
new file mode 100644
index 0000000..e76456f
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntity.java
@@ -0,0 +1,66 @@
+package com.groom.yummy.domain.reply;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.domain.group.GroupEntity;
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.reply.Reply;
+
+import jakarta.persistence.*;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Table(name="reply")
+@SuperBuilder
+public class ReplyEntity extends BaseEntity {
+
+ @Column(nullable = false)
+ private String content;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "parent_id")
+ private ReplyEntity parentReply;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private UserEntity user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "group_id", nullable = false)
+ private GroupEntity group;
+
+ @Builder
+ public ReplyEntity(String content, ReplyEntity parentReply, UserEntity user, GroupEntity group) {
+ this.content = content;
+ this.parentReply = parentReply;
+ this.user = user;
+ this.group = group;
+ }
+
+ public static Reply toModel(ReplyEntity entity) {
+ return Reply.builder()
+ .id(entity.getId())
+ .content(entity.getContent())
+ .parentReplyId(entity.getParentReply() != null ? entity.getParentReply().getId() : null)
+ .userId(entity.getUser().getId())
+ .groupId(entity.getGroup().getId())
+ .createdAt(entity.getCreatedAt())
+ .updatedAt(entity.getUpdatedAt())
+ .build();
+ }
+
+ public static ReplyEntity toEntity(Reply reply, ReplyEntity parent, UserEntity user, GroupEntity group) {
+ return ReplyEntity.builder()
+ .content(reply.getContent())
+ .parentReply(parent)
+ .user(user)
+ .group(group)
+ .build();
+ }
+
+ public void updateReply(String content) {
+ this.content = content;
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntityRepository.java
new file mode 100644
index 0000000..636009b
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyEntityRepository.java
@@ -0,0 +1,81 @@
+package com.groom.yummy.domain.reply;
+
+import java.util.Optional;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.groom.yummy.domain.group.GroupEntity;
+import com.groom.yummy.domain.group.GroupJpaRepository;
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.domain.user.UserJpaRepository;
+import com.groom.yummy.reply.Reply;
+import com.groom.yummy.reply.ReplyRepository;
+
+import lombok.RequiredArgsConstructor;
+
+@Repository
+@RequiredArgsConstructor
+public class ReplyEntityRepository implements ReplyRepository {
+
+ private final ReplyJpaRepository replyRepository;
+ private final UserJpaRepository userRepository;
+ private final GroupJpaRepository groupJpaRepository;
+
+ @Override
+ public Optional findById(Long id) {
+ return replyRepository.findById(id).map(ReplyEntity::toModel);
+ }
+
+ @Override
+ public Page findAllByParentId(Long parentId, Pageable pageable) {
+ return replyRepository.findByParentReplyId(parentId, pageable).map(ReplyEntity::toModel);
+ }
+
+ @Override
+ public Optional save(Reply reply) {
+ ReplyEntity parentReply = findParentReply(reply.getParentReplyId());
+ UserEntity user = findUser(reply.getUserId());
+ GroupEntity group = findGroup(reply.getGroupId());
+
+ ReplyEntity replyEntity = ReplyEntity.toEntity(reply, parentReply, user, group);
+ return Optional.ofNullable(ReplyEntity.toModel(replyRepository.save(replyEntity)));
+ }
+
+ @Override
+ public Page findByGroupId(Long groupId, Pageable pageable) {
+ return replyRepository.findByGroupId(groupId, pageable).map(ReplyEntity::toModel);
+ }
+
+ @Override
+ public void deleteById(Long id) {
+ // 자식 댓글 존재 여부 확인
+ if (replyRepository.existsByParentReplyId(id)) {
+ throw new IllegalStateException("자식 댓글이 존재하여 삭제할 수 없습니다. ID: " + id);
+ }
+
+ replyRepository.deleteById(id);
+ }
+
+ // 부모 댓글 조회
+ private ReplyEntity findParentReply(Long parentReplyId) {
+ if (parentReplyId == null) {
+ return null; // 부모 댓글이 없으면 null 반환
+ }
+ return replyRepository.findById(parentReplyId)
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 부모 댓글"));
+ }
+
+ // 사용자 조회
+ private UserEntity findUser(Long userId) {
+ return userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원"));
+ }
+
+ // 그룹 조회
+ private GroupEntity findGroup(Long groupId) {
+ return groupJpaRepository.findById(groupId)
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 그룹"));
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/reply/ReplyJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyJpaRepository.java
new file mode 100644
index 0000000..5108055
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/reply/ReplyJpaRepository.java
@@ -0,0 +1,15 @@
+package com.groom.yummy.domain.reply;
+
+import java.util.Optional;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ReplyJpaRepository extends JpaRepository {
+ Page findByGroupId(Long groupId, Pageable pageable);
+
+ Page findByParentReplyId(Long parentId, Pageable pageable);
+
+ boolean existsByParentReplyId(Long id);
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/store/StoreEntity.java b/storage/src/main/java/com/groom/yummy/domain/store/StoreEntity.java
new file mode 100644
index 0000000..ab6fc81
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/store/StoreEntity.java
@@ -0,0 +1,67 @@
+package com.groom.yummy.domain.store;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.domain.region.RegionEntity;
+import com.groom.yummy.store.Category_;
+import com.groom.yummy.store.Store;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Table(name = "store")
+@SuperBuilder
+public class StoreEntity extends BaseEntity {
+
+ @Column(nullable = false)
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Category_ category;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "region_id", nullable = false)
+ private RegionEntity region;
+
+ private StoreEntity(String name, Category_ category, RegionEntity region) {
+ this.name = name;
+ this.category = category;
+ this.region = region;
+ }
+
+ public static Store toStoreDomain(StoreEntity storeEntity) {
+ return Store.builder()
+ .storeId(storeEntity.getId())
+ .name(storeEntity.getName())
+ .regionId(storeEntity.getRegion().getId())
+ .category(storeEntity.getCategory())
+ .build();
+ }
+
+ public static StoreEntity fromStoreDomain(Store store, RegionEntity region) {
+ return StoreEntity.builder()
+ .name(store.getName())
+ .category(store.getCategory())
+ .region(region)
+ .build();
+ }
+
+ public static StoreEntity create(String name, Category_ category, RegionEntity region) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Store name must not be null or blank");
+ }
+ if (category == null) {
+ throw new IllegalArgumentException("Category must not be null");
+ }
+ if (region == null) {
+ throw new IllegalArgumentException("Region must not be null");
+ }
+ return new StoreEntity(name, category, region);
+ }
+
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/store/StoreEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/store/StoreEntityRepository.java
new file mode 100755
index 0000000..85f3f2a
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/store/StoreEntityRepository.java
@@ -0,0 +1,46 @@
+package com.groom.yummy.domain.store;
+
+import com.groom.yummy.domain.region.RegionEntity;
+import com.groom.yummy.domain.region.RegionJpaRepository;
+import com.groom.yummy.store.Store;
+import com.groom.yummy.store.StoreRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Repository
+@RequiredArgsConstructor
+public class StoreEntityRepository implements StoreRepository {
+
+ private final StoreJpaRepository storeJpaRepository;
+ private final RegionJpaRepository regionJpaRepository;
+
+ @Override
+ public Optional findStoreById(Long id) {
+ return storeJpaRepository.findById(id).map(StoreEntity::toStoreDomain);
+ }
+
+ @Override
+ public Long saveStore(Store store, Long regionId) {
+ RegionEntity regionEntity = regionJpaRepository.findById(regionId)
+ .orElseThrow(() -> new IllegalArgumentException("지역을 찾을 수 없습니다."));
+
+ StoreEntity storeEntity = StoreEntity.fromStoreDomain(store, regionEntity);
+ return storeJpaRepository.save(storeEntity).getId();
+ }
+
+ @Override
+ public List findAllStores() {
+ return storeJpaRepository.findAll().stream()
+ .map(StoreEntity::toStoreDomain)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public boolean existsByNameAndRegionId(String name, Long regionId) {
+ return storeJpaRepository.existsByNameAndRegion_Id(name, regionId);
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/store/StoreJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/store/StoreJpaRepository.java
new file mode 100644
index 0000000..9047aea
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/store/StoreJpaRepository.java
@@ -0,0 +1,7 @@
+package com.groom.yummy.domain.store;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StoreJpaRepository extends JpaRepository {
+ boolean existsByNameAndRegion_Id(String name, Long regionId);
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/user/UserEntity.java b/storage/src/main/java/com/groom/yummy/domain/user/UserEntity.java
new file mode 100644
index 0000000..e12948b
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/user/UserEntity.java
@@ -0,0 +1,69 @@
+package com.groom.yummy.domain.user;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.user.User;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Table(name = "users")
+@SuperBuilder
+public class UserEntity extends BaseEntity {
+
+ @Column(nullable = false, unique = true)
+ private String email;
+
+ @Column(nullable = false)
+ private String nickname;
+
+ @Column(nullable = false)
+ private Long groupJoinCount;
+
+ @Column(nullable = false)
+ private Long groupAttendanceCount;
+
+ @Column(nullable = false)
+ private boolean isDeleted;
+
+ @Column(nullable = false)
+ private String role;
+
+ public void updateNickname(String nickname){
+ this.nickname = nickname;
+ }
+
+ public void deleteUser(boolean isDeleted){
+ this.isDeleted = isDeleted;
+ }
+
+ public static User toModel(UserEntity userEntity){
+ return User.builder()
+ .id(userEntity.getId())
+ .nickname(userEntity.nickname)
+ .email(userEntity.getEmail())
+ .role(userEntity.role)
+ .groupAttendanceCount(userEntity.groupAttendanceCount)
+ .groupJoinCount(userEntity.groupJoinCount)
+ .isDeleted(userEntity.isDeleted)
+ .build();
+ }
+
+ public static UserEntity toEntity(User user){
+ return UserEntity.builder()
+ .nickname(user.getNickname())
+ .email(user.getEmail())
+ .role(user.getRole())
+ .groupAttendanceCount(user.getGroupAttendanceCount())
+ .groupJoinCount(user.getGroupJoinCount())
+ .isDeleted(user.isDeleted())
+ .build();
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/user/UserEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/user/UserEntityRepository.java
new file mode 100644
index 0000000..81ffa3b
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/user/UserEntityRepository.java
@@ -0,0 +1,30 @@
+package com.groom.yummy.domain.user;
+
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class UserEntityRepository implements UserRepository {
+ private final UserJpaRepository userJpaRepository;
+
+ @Override
+ public Optional findById(Long userId) {
+ return userJpaRepository.findById(userId).map(UserEntity::toModel);
+ }
+
+ @Override
+ public Optional findByEmail(String email) {
+ return userJpaRepository.findByEmail(email).map(UserEntity::toModel);
+ }
+
+ @Override
+ public User save(User user) {
+ UserEntity userEntity = userJpaRepository.save(UserEntity.toEntity(user));
+ return UserEntity.toModel(userEntity);
+ }
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/user/UserJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/user/UserJpaRepository.java
new file mode 100644
index 0000000..9468d0d
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/user/UserJpaRepository.java
@@ -0,0 +1,9 @@
+package com.groom.yummy.domain.user;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserJpaRepository extends JpaRepository {
+ Optional findByEmail(String email);
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntity.java b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntity.java
new file mode 100644
index 0000000..10b821c
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntity.java
@@ -0,0 +1,55 @@
+package com.groom.yummy.domain.usertogroup;
+
+import com.groom.yummy.domain.BaseEntity;
+import com.groom.yummy.domain.group.GroupEntity;
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.usertogroup.AttendanceStatus;
+import jakarta.persistence.*;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(name = "user_groups")
+@SuperBuilder
+@Getter
+public class UserToGroupEntity extends BaseEntity {
+
+// @Id
+// @GeneratedValue(strategy = GenerationType.IDENTITY)
+// private Long id;
+
+ @Column(nullable = false)
+ private boolean isLeader;
+
+ @Getter
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private AttendanceStatus attendanceStatus;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private UserEntity user;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "group_id", nullable = false)
+ private GroupEntity group;
+
+ @Builder
+ public UserToGroupEntity(AttendanceStatus attendanceStatus, boolean isLeader, UserEntity user, GroupEntity group) {
+ this.attendanceStatus = attendanceStatus;
+ this.isLeader = isLeader;
+ this.user = user;
+ this.group = group;
+ }
+
+ public static UserToGroupEntity create(GroupEntity groupEntity, UserEntity userEntity, boolean isLeader) {
+ return UserToGroupEntity.builder()
+ .group(groupEntity)
+ .user(userEntity)
+ .isLeader(isLeader)
+ .attendanceStatus(AttendanceStatus.APPROVED)
+ .build();
+ }
+
+}
diff --git a/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntityRepository.java b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntityRepository.java
new file mode 100644
index 0000000..14f5f3d
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupEntityRepository.java
@@ -0,0 +1,64 @@
+package com.groom.yummy.domain.usertogroup;
+
+import com.groom.yummy.domain.group.GroupEntity;
+import com.groom.yummy.domain.group.GroupJpaRepository;
+import com.groom.yummy.domain.store.StoreEntity;
+import com.groom.yummy.domain.store.StoreJpaRepository;
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.domain.user.UserJpaRepository;
+import com.groom.yummy.group.Group;
+import com.groom.yummy.store.Store;
+import com.groom.yummy.user.User;
+import com.groom.yummy.user.UserRepository;
+import com.groom.yummy.usertogroup.AttendanceStatus;
+import com.groom.yummy.usertogroup.UserToGroup;
+import com.groom.yummy.usertogroup.UserToGroupRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class UserToGroupEntityRepository implements UserToGroupRepository {
+
+ private final UserToGroupJpaRepository userToGroupJpaRepository;
+ private final GroupJpaRepository groupJpaRepository;
+ private final UserJpaRepository userJpaRepository;
+
+ @Override
+ public void saveUserToGroup(Long groupId, Long userId, boolean isLeader, AttendanceStatus attendanceStatus) {
+// StoreEntity storeEntity = group.getStoreId() != null
+// ? storeJpaRepository.findById(group.getStoreId())
+// .orElseThrow(() -> new IllegalArgumentException("Store not found for id: " + group.getStoreId()))
+// : null;
+
+ GroupEntity groupEntity = groupJpaRepository.findById(groupId).orElseThrow();
+ UserEntity userEntity = userJpaRepository.findById(userId).orElseThrow();
+
+ UserToGroupEntity userToGroupEntity = UserToGroupEntity.builder()
+ .group(groupEntity)
+ .user(userEntity)
+ .isLeader(isLeader)
+ .attendanceStatus(attendanceStatus)
+ .build();
+
+ userToGroupJpaRepository.save(userToGroupEntity);
+ }
+
+ @Override
+ public Optional findByGroupAndUser(Group group, User user) {
+ GroupEntity groupEntity = GroupEntity.fromGroupDomain(group);
+// UserEntity userEntity = UserEntity.toEntity(user);
+
+ return userToGroupJpaRepository.findById(groupEntity.getId())
+ .map(entity -> UserToGroup.builder()
+ .id(entity.getId())
+ .group(group)
+ .user(user)
+ .isLeader(entity.isLeader())
+ .attendanceStatus(entity.getAttendanceStatus())
+ .build());
+ }
+}
+
diff --git a/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupJpaRepository.java b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupJpaRepository.java
new file mode 100644
index 0000000..1622ddf
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/domain/usertogroup/UserToGroupJpaRepository.java
@@ -0,0 +1,11 @@
+package com.groom.yummy.domain.usertogroup;
+
+import com.groom.yummy.domain.group.GroupEntity;
+import com.groom.yummy.domain.user.UserEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserToGroupJpaRepository extends JpaRepository {
+ Optional findByGroupAndUser(GroupEntity group, UserEntity user);
+}
diff --git a/storage/src/main/java/com/groom/yummy/handler/ReplyEventListener.java b/storage/src/main/java/com/groom/yummy/handler/ReplyEventListener.java
new file mode 100644
index 0000000..0afd178
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/handler/ReplyEventListener.java
@@ -0,0 +1,28 @@
+package com.groom.yummy.handler;
+
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import com.groom.yummy.domain.reply.ReplyEntity;
+import com.groom.yummy.domain.reply.ReplyJpaRepository;
+import com.groom.yummy.reply.event.ReplyUpdatedEvent;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ReplyEventListener {
+
+ private final ReplyJpaRepository replyJpaRepository;
+
+ @EventListener
+ public void handleReplyUpdatedEvent(ReplyUpdatedEvent event) {
+ ReplyEntity replyEntity = replyJpaRepository.findById(event.replyId())
+ .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글의 아이디"));
+
+ replyEntity.updateReply(event.content());
+ log.info("댓글이 수정되었습니다. ID: {}, 내용: {}", event.replyId(), event.content());
+ }
+}
\ No newline at end of file
diff --git a/storage/src/main/java/com/groom/yummy/handler/UserEventHandler.java b/storage/src/main/java/com/groom/yummy/handler/UserEventHandler.java
new file mode 100644
index 0000000..c680305
--- /dev/null
+++ b/storage/src/main/java/com/groom/yummy/handler/UserEventHandler.java
@@ -0,0 +1,32 @@
+package com.groom.yummy.handler;
+
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.domain.user.UserJpaRepository;
+import com.groom.yummy.user.event.UserDeleteEvent;
+import com.groom.yummy.user.event.UserNicknameChangedEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@RequiredArgsConstructor
+@Slf4j
+@Component
+public class UserEventHandler {
+ private final UserJpaRepository userJpaRepository;
+
+ @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+ public void handlerNicknameChange(UserNicknameChangedEvent event){
+ UserEntity userEntity = userJpaRepository.findById(event.userId()).orElseThrow();
+ userEntity.updateNickname(event.newNickname());
+ log.info("userEntity nickname : {}", userEntity.getNickname());
+ }
+
+ @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
+ public void handleUserDelete(UserDeleteEvent event) {
+ UserEntity userEntity = userJpaRepository.findById(event.userId()).orElseThrow();
+ userEntity.deleteUser(event.isDeleted());
+ log.info("userEntity isDelete : {}", userEntity.isDeleted());
+ }
+}
diff --git a/storage/src/test/java/com/groom/yummy/TestApplication.java b/storage/src/test/java/com/groom/yummy/TestApplication.java
new file mode 100644
index 0000000..e7029c8
--- /dev/null
+++ b/storage/src/test/java/com/groom/yummy/TestApplication.java
@@ -0,0 +1,18 @@
+package com.groom.yummy;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+// 테스트 환경에서 mock 테스트가 아닌 실제 db(H2 db)에 접근하여 테스트 하기 위해 추가.
+@SpringBootApplication
+public class TestApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(TestApplication.class, args);
+ }
+}
+
+/* 컴포넌트 스캔의 범위
+ 예를 들어, TestApplication 클래스가 src/test/java/com.groom.yummy 패키지에 위치하면:
+ 스캔 범위: com.groom.yummy와 그 하위 패키지.
+ 따라서 main 폴더 아래의 com.groom.yummy에 위치한 클래스들도 빈으로 등록됩니다.
+ */
diff --git a/storage/src/test/java/com/groom/yummy/domain/user/UserEntityRepositoryTest.java b/storage/src/test/java/com/groom/yummy/domain/user/UserEntityRepositoryTest.java
new file mode 100644
index 0000000..f319db3
--- /dev/null
+++ b/storage/src/test/java/com/groom/yummy/domain/user/UserEntityRepositoryTest.java
@@ -0,0 +1,92 @@
+package com.groom.yummy.domain.user;
+
+import com.groom.yummy.user.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@SpringBootTest
+@ActiveProfiles("test")
+@Transactional
+class UserEntityRepositoryTest {
+
+ @Autowired
+ private UserEntityRepository userEntityRepository;
+
+ @Autowired
+ private UserJpaRepository userJpaRepository;
+
+ private UserEntity testUserEntity;
+
+ @BeforeEach
+ void setUp(){
+ testUserEntity = UserEntity.builder()
+ .email("email@gmail.com")
+ .nickname("강형준")
+ .role("ROLE_USER")
+ .groupAttendanceCount(0L)
+ .groupJoinCount(0L)
+ .isDeleted(false)
+ .build();
+
+ userJpaRepository.save(testUserEntity);
+ }
+
+ @Test
+ void findByIdTest() {
+ // given
+ Long userId = testUserEntity.getId();
+ // when
+ Optional result = userEntityRepository.findById(userId);
+ // then
+ assertTrue(result.isPresent());
+ assertEquals(testUserEntity.getId(), result.get().getId());
+ assertEquals(testUserEntity.getEmail(), result.get().getEmail());
+ assertEquals(testUserEntity.getNickname(), result.get().getNickname());
+ assertEquals(testUserEntity.getRole(), result.get().getRole());
+ assertEquals(testUserEntity.isDeleted(), result.get().isDeleted());
+ }
+
+ @Test
+ void findByEmailTest() {
+ // given
+ String email = "email@gmail.com";
+
+ // when
+ Optional result = userEntityRepository.findByEmail(email);
+
+ // then
+ assertTrue(result.isPresent());
+ assertEquals(testUserEntity.getId(),result.get().getId());
+ assertEquals(testUserEntity.getEmail(), result.get().getEmail());
+ assertEquals(testUserEntity.getNickname(), result.get().getNickname());
+ assertEquals(testUserEntity.getRole(), result.get().getRole());
+ }
+
+ @Test
+ void saveTest() {
+ // given
+ String email = "email@naver.com";
+ String nickname = "홍길동";
+ String role = "ROLE_USER";
+ boolean isDeleted = false;
+ User user = User.builder().email(email).nickname(nickname).role(role).groupJoinCount(0L).groupAttendanceCount(0L).isDeleted(isDeleted).build();
+
+ // when
+ User result = userEntityRepository.save(user);
+
+ // then
+ assertEquals(result.getEmail(), email);
+ assertEquals(result.getNickname(), nickname);
+ assertEquals(result.getRole(), role);
+ assertEquals(result.isDeleted(), isDeleted);
+ }
+}
\ No newline at end of file
diff --git a/storage/src/test/java/com/groom/yummy/handler/UserEventHandlerTest.java b/storage/src/test/java/com/groom/yummy/handler/UserEventHandlerTest.java
new file mode 100644
index 0000000..9d37935
--- /dev/null
+++ b/storage/src/test/java/com/groom/yummy/handler/UserEventHandlerTest.java
@@ -0,0 +1,87 @@
+package com.groom.yummy.handler;
+
+import com.groom.yummy.domain.user.UserEntity;
+import com.groom.yummy.domain.user.UserJpaRepository;
+import com.groom.yummy.user.event.UserDeleteEvent;
+import com.groom.yummy.user.event.UserNicknameChangedEvent;
+import jakarta.persistence.EntityManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.test.context.transaction.TestTransaction;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest
+@Transactional
+class UserEventHandlerTest {
+
+ @Autowired
+ private ApplicationEventPublisher eventPublisher;
+
+ @Autowired
+ private UserJpaRepository userJpaRepository;
+
+ private UserEntity testUserEntity;
+
+ @BeforeEach
+ void setUp(){
+ testUserEntity = UserEntity.builder()
+ .email("email@gmail.com")
+ .nickname("강형준")
+ .role("ROLE_USER")
+ .groupAttendanceCount(0L)
+ .groupJoinCount(0L)
+ .isDeleted(false)
+ .build();
+ userJpaRepository.save(testUserEntity);
+ }
+
+ @Test
+ @DirtiesContext //테스트 실행 후에 Spring 애플리케이션 컨텍스트를 재생성, 트랜잭션 더러워져서 발생한 문제 일단 해결
+ void handlerNicknameChaneTest(){
+ // given
+ Long userId = testUserEntity.getId();
+ String newNickname = "새로운강형준";
+
+ // when
+ eventPublisher.publishEvent(new UserNicknameChangedEvent(userId, newNickname));
+
+ // 해당 로직은 커밋 이전에 반영되모르 명시적으로 커밋을 설정한다.
+ TestTransaction.flagForCommit();
+ TestTransaction.end();
+
+ // then
+ Optional updatedUser = userJpaRepository.findById(userId);
+ assertTrue(updatedUser.isPresent());
+ assertEquals(newNickname, updatedUser.get().getNickname());
+ }
+
+ @Test
+ @DirtiesContext //테스트 실행 후에 Spring 애플리케이션 컨텍스트를 재생성, 트랜잭션 더러워져서 발생한 문제 일단 해결
+ void handleUserDeleteTest(){
+
+ // given
+ Long userId = testUserEntity.getId();
+ boolean isDeleted = true;
+
+ // when
+ eventPublisher.publishEvent(new UserDeleteEvent(userId, isDeleted));
+
+ // 해당 로직은 커밋 이전에 반영되모르 명시적으로 커밋을 설정한다.
+ TestTransaction.flagForCommit();
+ TestTransaction.end();
+
+ // then
+ Optional deletedUser = userJpaRepository.findById(userId);
+ assertTrue(deletedUser.isPresent());
+ assertTrue(deletedUser.get().isDeleted());
+ }
+}
\ No newline at end of file