diff --git a/.github/workflows/cicd.yml b/.github/workflows/dev-cicd.yml
similarity index 95%
rename from .github/workflows/cicd.yml
rename to .github/workflows/dev-cicd.yml
index b6ed4fe..561ded5 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/dev-cicd.yml
@@ -1,4 +1,4 @@
-name: Onetime CI/CD with Gradle
+name: Onetime DEV CI/CD with Gradle
on:
push:
@@ -36,7 +36,7 @@ jobs:
run: chmod +x ./gradlew
- name: ⏱️gradle build 중입니다.
- run: ./gradlew build -x test
+ run: ./gradlew clean build openapi3 asciidoctor
- name: ⏱️Docker Hub에 로그인합니다.
uses: docker/login-action@v2
diff --git a/.github/workflows/test-cicd.yml b/.github/workflows/test-cicd.yml
new file mode 100644
index 0000000..9990e24
--- /dev/null
+++ b/.github/workflows/test-cicd.yml
@@ -0,0 +1,61 @@
+name: Onetime TEST CI/CD with Gradle
+
+on:
+ push:
+ branches: [ "test" ]
+ pull_request:
+ branches: [ "test" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: ⏱️Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: ⏱️Gradle Caching - 빌드 시간 향상
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: ⏱️gradle build를 위한 권한을 부여합니다.
+ run: chmod +x ./gradlew
+
+ - name: ⏱️gradle build 중입니다.
+ run: ./gradlew clean build openapi3 asciidoctor
+
+ - name: ⏱️Docker Hub에 로그인합니다.
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+
+ - name: ⏱️Docker image build 후 Docker hub에 push합니다.
+ run: |
+ docker build -f Dockerfile -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }} .
+ docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}
+
+ - name: ⏱️Docker hub에서 pull 후 deploy합니다.
+ uses: appleboy/ssh-action@master
+ with:
+ host: ${{ secrets.GCP_TEST_SERVER_HOST }}
+ username: ${{ secrets.GCP_TEST_SERVER_USERNAME }}
+ key: ${{ secrets.GCP_SSH_KEY }}
+ script: |
+ sudo chmod 777 ./deploy.sh
+ ./deploy.sh
+ sudo docker image prune -f
\ No newline at end of file
diff --git a/README.md b/README.md
index 18803d0..ed99e68 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,69 @@
-# backend
+# OneTime Backend 🚀
+
+## 🌐 System Architecture
+
+
+
+## 🧱 ERD
+
+
+
+
+## 📄 API Documentation
+
+[📝 REST Docs + Swagger](https://onetime-test.store/swagger-ui/index.html#/)
+
+## 🔒 Rules
+
+### Branch
+
+- 생성한 이슈에 따라서 브랜치 생성 `Ex) feature/#4/login`
+- `main branch` : 개발 최종 완료 시 merge
+- `develop branch` : 배포 서버용
+- `test branch` : 테스트 서버용
+- `feature branch` : 각 새로운 기능 개발
+- `hotfix branch` : 배포 이후 긴급 수정
+
+### **Commit Message**
+
+- 이슈 번호 붙여서 커밋 `Ex) #4 [feat] : 로그인 기능을 추가한다`
+- Body는 추가 설명 필요하면 사용
+
+| ***작업태그*** | ***내용*** |
+| --- | --- |
+| **feat** | 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 |
+| **fix** | 버그 수정 |
+| **refactor** | 코드 리팩토링 |
+| **style** | 코드 의미에 영향을 주지 않는 변경사항 (코드 포맷팅, 오타 수정, 변수명 변경, 에셋 추가) |
+| **chore** | 빌드 부분 혹은 패키지 매니저 수정 사항 / 파일 이름 변경 및 위치 변경 / 파일 삭제 |
+| **docs** | 문서 추가 및 수정 |
+| **rename** | 패키지 혹은 폴더명, 클래스명 수정 (단독으로 시행하였을 시) |
+| **remove** | 패키지 혹은 폴더, 클래스를 삭제하였을 때 (단독으로 시행하였을 시) |
+
+### Naming
+
+- **패키지명** : 한 단어 소문자 사용 `Ex) service`
+- **클래스명** : 파스칼 케이스 사용 `Ex) JwtUtil`
+- **메서드명** : 카멜 케이스 사용, 동사로 시작 `Ex) getUserScraps`
+- **변수명** : 카멜 케이스 사용 `Ex) jwtToken`
+- **상수명** : 대문자 사용 `Ex) EXPIRATION_TIME`
+- **컬럼명** : 스네이크 케이스 사용 `Ex) user_id`
+
+
+### API Response
+
+```json
+{
+ "code": "201",
+ "message": "이벤트 생성에 성공했습니다.",
+ "payload": {
+ "event_id": "5e35b658-ee4b-4c52-98dc-94b79f0e64c9"
+ },
+ "is_success": true
+}
+```
+
+- `is_success` : 성공 여부
+- `code` : 성공 코드, HTTP 상태 코드와 동일
+- `message` : 성공 메세지
+- `payload` : 데이터가 들어가는 곳
diff --git a/build.gradle b/build.gradle
index 05639e9..379998c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,11 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
+
+ // REST Docs
+ id "org.asciidoctor.jvm.convert" version "3.3.2"
+ id 'com.epages.restdocs-api-spec' version '0.19.2'
+ id 'org.hidetake.swagger.generator' version '2.18.2'
}
group = 'side'
@@ -50,10 +55,75 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.2'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.2'
+ // REST Docs & Swagger
+ testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.19.2'
+ testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:3.0.0'
+ testImplementation 'com.squareup.okhttp3:mockwebserver'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
+}
+
+// QueryDSL 디렉토리
+def querydslDir = "src/main/generated"
+
+// REST Docs & Swagger 설정
+ext {
+ snippetsDir = file('build/generated-snippets')
}
tasks.named('test') {
useJUnitPlatform()
+ outputs.dir snippetsDir
+}
+
+sourceSets {
+ test {
+ java {
+ srcDirs = ['src/test/java']
+ }
+ }
+}
+
+def serverUrl = "https://onetime-test.store"
+
+openapi3 {
+ server = serverUrl
+ title = "OneTime API Documentation"
+ description = "Spring REST Docs with Swagger UI."
+ version = "0.0.1"
+ outputFileNamePrefix = 'open-api-3.0.1'
+ format = 'json'
+ outputDirectory = 'build/resources/main/static/docs'
+}
+
+tasks.withType(GenerateSwaggerUI).configureEach {
+ dependsOn 'openapi3'
+
+ delete file('src/main/resources/static/docs/')
+ copy {
+ from "build/resources/main/static/docs"
+ into "src/main/resources/static/docs/"
+ }
+}
+
+tasks.named('asciidoctor') {
+ inputs.dir snippetsDir
+ dependsOn test
+}
+
+tasks.named("bootJar") {
+ dependsOn asciidoctor
+ from("${asciidoctor.outputDir}") {
+ into 'static/docs'
+ }
+ dependsOn ':openapi3'
+}
+
+tasks.register('copyDocument', Copy) {
+ dependsOn asciidoctor
+ from file(project.layout.buildDirectory.dir("docs/asciidoc").get().asFile.path)
+ into file("src/main/resources/static/docs")
}
-def querydslDir = "src/main/generated"
\ No newline at end of file
+tasks.named("build") {
+ dependsOn copyDocument
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/EventController.java b/src/main/java/side/onetime/controller/EventController.java
index b5f709a..f88ee8a 100644
--- a/src/main/java/side/onetime/controller/EventController.java
+++ b/src/main/java/side/onetime/controller/EventController.java
@@ -5,6 +5,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import side.onetime.dto.event.request.CreateEventRequest;
+import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest;
import side.onetime.dto.event.response.*;
import side.onetime.global.common.ApiResponse;
import side.onetime.global.common.status.SuccessStatus;
@@ -18,7 +19,16 @@
public class EventController {
private final EventService eventService;
- // 이벤트 생성 API
+ /**
+ * 이벤트 생성 API
+ *
+ * 이 API는 새로운 이벤트를 생성합니다. 인증된 유저와 익명 유저 모두 이벤트를 생성할 수 있으며,
+ * 인증된 유저의 경우 추가적인 정보가 저장됩니다.
+ *
+ * @param createEventRequest 생성할 이벤트에 대한 요청 데이터 (제목, 시작/종료 시간, 카테고리, 설문 범위 등)
+ * @param authorizationHeader 인증된 유저의 토큰 (선택 사항)
+ * @return 생성된 이벤트의 ID
+ */
@PostMapping
public ResponseEntity> createEvent(
@Valid @RequestBody CreateEventRequest createEventRequest,
@@ -34,7 +44,16 @@ public ResponseEntity> createEvent(
return ApiResponse.onSuccess(SuccessStatus._CREATED_EVENT, createEventResponse);
}
- // 이벤트 조회 API
+ /**
+ * 이벤트 조회 API
+ *
+ * 이 API는 특정 이벤트의 세부 정보를 조회합니다. 이벤트의 제목, 시간, 카테고리 등의 정보를 제공하며
+ * 인증된 유저일 경우 추가적인 정보가 포함될 수 있습니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰 (선택 사항)
+ * @param eventId 조회할 이벤트의 ID
+ * @return 조회한 이벤트의 세부 정보
+ */
@GetMapping("/{event_id}")
public ResponseEntity> getEvent(
@RequestHeader(value = "Authorization", required = false) String authorizationHeader,
@@ -42,11 +61,17 @@ public ResponseEntity> getEvent(
GetEventResponse getEventResponse = eventService.getEvent(eventId, authorizationHeader);
-
return ApiResponse.onSuccess(SuccessStatus._GET_EVENT, getEventResponse);
}
- // 참여자 조회 API
+ /**
+ * 참여자 조회 API
+ *
+ * 이 API는 특정 이벤트에 참여한 모든 참여자의 이름 목록을 조회합니다.
+ *
+ * @param eventId 참여자 목록을 조회할 이벤트의 ID
+ * @return 해당 이벤트에 참여한 참여자의 이름 목록
+ */
@GetMapping("/{event_id}/participants")
public ResponseEntity> getParticipants(
@PathVariable("event_id") String eventId) {
@@ -55,7 +80,14 @@ public ResponseEntity> getParticipants(
return ApiResponse.onSuccess(SuccessStatus._GET_PARTICIPANTS, getParticipantsResponse);
}
- // 가장 많이 되는 시간 조회 API
+ /**
+ * 가장 많이 되는 시간 조회 API
+ *
+ * 이 API는 특정 이벤트에서 가장 많이 가능한 시간대를 조회하여, 가능 인원과 해당 시간대 정보를 제공합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @return 가능 인원이 많은 시간대와 관련 세부 정보
+ */
@GetMapping("/{event_id}/most")
public ResponseEntity>> getMostPossibleTime(
@PathVariable("event_id") String eventId) {
@@ -64,7 +96,14 @@ public ResponseEntity>> getMostPossibleTim
return ApiResponse.onSuccess(SuccessStatus._GET_MOST_POSSIBLE_TIME, getMostPossibleTimes);
}
- // 유저 참여 이벤트 목록 조회 API
+ /**
+ * 유저 참여 이벤트 목록 조회 API
+ *
+ * 이 API는 인증된 유저가 참여한 모든 이벤트 목록을 조회합니다. 유저의 참여 상태, 이벤트 정보 등이 포함됩니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 유저가 참여한 이벤트 목록
+ */
@GetMapping("/user/all")
public ResponseEntity>> getUserParticipatedEvents(
@RequestHeader("Authorization") String authorizationHeader) {
@@ -73,7 +112,15 @@ public ResponseEntity>> getU
return ApiResponse.onSuccess(SuccessStatus._GET_USER_PARTICIPATED_EVENTS, getUserParticipatedEventsResponses);
}
- // 유저가 생성한 이벤트 삭제 API
+ /**
+ * 유저가 생성한 이벤트 삭제 API
+ *
+ * 이 API는 인증된 유저가 생성한 특정 이벤트를 삭제합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param eventId 삭제할 이벤트의 ID
+ * @return 삭제 성공 여부
+ */
@DeleteMapping("/{event_id}")
public ResponseEntity> removeUserCreatedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@@ -82,4 +129,24 @@ public ResponseEntity> removeUserCreatedEvent(
eventService.removeUserCreatedEvent(authorizationHeader, eventId);
return ApiResponse.onSuccess(SuccessStatus._REMOVE_USER_CREATED_EVENT);
}
-}
+
+ /**
+ * 유저가 생성한 이벤트 제목 수정 API
+ *
+ * 이 API는 인증된 유저가 생성한 특정 이벤트의 제목을 수정합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param eventId 제목을 수정할 이벤트의 ID
+ * @param modifyUserCreatedEventTitleRequest 새로운 제목 정보가 담긴 요청 데이터
+ * @return 수정 성공 여부
+ */
+ @PutMapping("/{event_id}")
+ public ResponseEntity> modifyUserCreatedEventTitle(
+ @RequestHeader("Authorization") String authorizationHeader,
+ @PathVariable("event_id") String eventId,
+ @Valid @RequestBody ModifyUserCreatedEventTitleRequest modifyUserCreatedEventTitleRequest) {
+
+ eventService.modifyUserCreatedEventTitle(authorizationHeader, eventId, modifyUserCreatedEventTitleRequest);
+ return ApiResponse.onSuccess(SuccessStatus._MODIFY_USER_CREATED_EVENT_TITLE);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/FixedController.java b/src/main/java/side/onetime/controller/FixedController.java
index d71f368..152a877 100644
--- a/src/main/java/side/onetime/controller/FixedController.java
+++ b/src/main/java/side/onetime/controller/FixedController.java
@@ -23,39 +23,68 @@ public class FixedController {
private final FixedEventService fixedEventService;
private final FixedScheduleService fixedScheduleService;
- // 고정 이벤트 생성 및 고정 스케줄 등록 API
+ /**
+ * 고정 이벤트 생성 및 고정 스케줄 등록 API
+ *
+ * 이 API는 새로운 고정 이벤트를 생성하고 관련된 고정 스케줄을 등록합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param createFixedEventRequest 생성할 고정 이벤트에 대한 요청 데이터 (제목, 스케줄 목록 등)
+ * @return 생성 성공 여부를 나타내는 메시지
+ */
@PostMapping
public ResponseEntity> createFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@Valid @RequestBody CreateFixedEventRequest createFixedEventRequest) {
fixedEventService.createFixedEvent(authorizationHeader, createFixedEventRequest);
-
return ApiResponse.onSuccess(SuccessStatus._CREATED_FIXED_SCHEDULE);
}
- // 전체 고정 스케줄 조회 API
+ /**
+ * 전체 고정 스케줄 조회 API
+ *
+ * 이 API는 유저가 등록한 모든 고정 스케줄을 조회합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 유저가 등록한 모든 고정 스케줄 목록
+ */
@GetMapping
public ResponseEntity>> getAllFixedSchedules(
@RequestHeader("Authorization") String authorizationHeader) {
List fixedEventResponses = fixedScheduleService.getAllFixedSchedules(authorizationHeader);
-
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_FIXED_SCHEDULES, fixedEventResponses);
}
- // 특정 고정 스케줄 상세 조회 API
+ /**
+ * 특정 고정 스케줄 상세 조회 API
+ *
+ * 이 API는 특정 ID에 해당하는 고정 스케줄의 상세 정보를 조회합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param fixedEventId 조회할 고정 스케줄의 ID
+ * @return 조회된 고정 스케줄의 세부 정보
+ */
@GetMapping("/{id}")
public ResponseEntity> getFixedScheduleDetail(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("id") Long fixedEventId) {
FixedEventDetailResponse fixedEventDetailResponse = fixedScheduleService.getFixedScheduleDetail(authorizationHeader, fixedEventId);
-
return ApiResponse.onSuccess(SuccessStatus._GET_FIXED_SCHEDULE_DETAIL, fixedEventDetailResponse);
}
- // 고정 이벤트 또는 스케줄 수정 API
+ /**
+ * 고정 이벤트 또는 스케줄 수정 API
+ *
+ * 이 API는 특정 고정 이벤트의 제목과 스케줄을 수정할 수 있습니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param fixedEventId 수정할 고정 이벤트의 ID
+ * @param modifyFixedEventRequest 수정할 고정 이벤트의 제목 및 스케줄
+ * @return 수정 성공 여부를 나타내는 메시지
+ */
@PatchMapping("/{id}")
public ResponseEntity> modifyFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@@ -72,25 +101,39 @@ public ResponseEntity> modifyFixedEvent(
return ApiResponse.onSuccess(SuccessStatus._MODIFY_FIXED_SCHEDULE);
}
- // 고정 이벤트 & 스케줄 삭제 API
+ /**
+ * 고정 이벤트 & 스케줄 삭제 API
+ *
+ * 이 API는 특정 ID에 해당하는 고정 이벤트와 관련된 스케줄을 삭제합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param fixedEventId 삭제할 고정 이벤트의 ID
+ * @return 삭제 성공 여부를 나타내는 메시지
+ */
@DeleteMapping("/{id}")
public ResponseEntity> removeFixedEvent(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("id") Long fixedEventId) {
fixedEventService.removeFixedEvent(authorizationHeader, fixedEventId);
-
return ApiResponse.onSuccess(SuccessStatus._REMOVE_FIXED_SCHEDULE);
}
- // 요일별 고정 이벤트 조회 API
+ /**
+ * 요일별 고정 이벤트 조회 API
+ *
+ * 이 API는 특정 요일에 해당하는 고정 이벤트 목록을 조회합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param day 조회할 요일 (예: 월, 화 등)
+ * @return 조회된 요일의 고정 이벤트 목록
+ */
@GetMapping("by-day/{day}")
public ResponseEntity>> getFixedEventByDay(
@RequestHeader("Authorization") String authorizationHeader,
@PathVariable("day") String day) {
List response = fixedEventService.getFixedEventByDay(authorizationHeader, day);
-
return ApiResponse.onSuccess(SuccessStatus._GET_FIXED_EVENT_BY_DAY, response);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/MemberController.java b/src/main/java/side/onetime/controller/MemberController.java
index 6ba7f6b..baed016 100644
--- a/src/main/java/side/onetime/controller/MemberController.java
+++ b/src/main/java/side/onetime/controller/MemberController.java
@@ -23,7 +23,14 @@
public class MemberController {
private final MemberService memberService;
- // 멤버 등록 API
+ /**
+ * 멤버 등록 API
+ *
+ * 이 API는 새로운 멤버를 등록합니다. 멤버가 속한 이벤트 ID와 이름, PIN, 스케줄 목록을 받습니다.
+ *
+ * @param registerMemberRequest 등록할 멤버 정보 (이벤트 ID, 이름, PIN, 스케줄 목록)
+ * @return 성공 여부와 등록된 멤버 정보 (멤버 ID, 이벤트 카테고리)
+ */
@PostMapping("/action-register")
public ResponseEntity> registerMember(
@Valid @RequestBody RegisterMemberRequest registerMemberRequest) {
@@ -32,7 +39,14 @@ public ResponseEntity> registerMember(
return ApiResponse.onSuccess(SuccessStatus._REGISTER_MEMBER, registerMemberResponse);
}
- // 멤버 로그인 API
+ /**
+ * 멤버 로그인 API
+ *
+ * 이 API는 멤버의 로그인 정보를 확인하고, 로그인에 성공한 경우 멤버의 정보를 반환합니다.
+ *
+ * @param loginMemberRequest 로그인할 멤버 정보 (이벤트 ID, 이름, PIN)
+ * @return 성공 여부와 로그인된 멤버 정보 (멤버 ID, 이벤트 카테고리)
+ */
@PostMapping("/action-login")
public ResponseEntity> loginMember(
@Valid @RequestBody LoginMemberRequest loginMemberRequest) {
@@ -41,7 +55,14 @@ public ResponseEntity> loginMember(
return ApiResponse.onSuccess(SuccessStatus._LOGIN_MEMBER, loginMemberResponse);
}
- // 이름 중복 확인 API
+ /**
+ * 이름 중복 확인 API
+ *
+ * 이 API는 특정 이벤트에서 지정한 이름이 중복되는지 확인합니다.
+ *
+ * @param isDuplicateRequest 중복 확인할 정보 (이벤트 ID, 확인할 이름)
+ * @return 성공 여부와 이름 사용 가능 여부 (isPossible 필드)
+ */
@PostMapping("/name/action-check")
public ResponseEntity> isDuplicate(
@Valid @RequestBody IsDuplicateRequest isDuplicateRequest) {
@@ -49,4 +70,4 @@ public ResponseEntity> isDuplicate(
IsDuplicateResponse isDuplicateResponse = memberService.isDuplicate(isDuplicateRequest);
return ApiResponse.onSuccess(SuccessStatus._IS_POSSIBLE_NAME, isDuplicateResponse);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/ScheduleController.java b/src/main/java/side/onetime/controller/ScheduleController.java
index 944aea0..27fbd49 100644
--- a/src/main/java/side/onetime/controller/ScheduleController.java
+++ b/src/main/java/side/onetime/controller/ScheduleController.java
@@ -21,7 +21,16 @@
public class ScheduleController {
private final ScheduleService scheduleService;
- // 요일 스케줄 등록 API
+ /**
+ * 요일 스케줄 등록 API
+ *
+ * 요일별 반복되는 스케줄을 등록하는 API입니다.
+ * 인증된 사용자와 비인증 사용자에 따라 스케줄 생성 방식이 다릅니다.
+ *
+ * @param createDayScheduleRequest 요일 스케줄 생성 요청 객체 (이벤트 ID, 멤버 ID, 요일 스케줄 목록)
+ * @param authorizationHeader 인증된 유저의 토큰 (선택사항)
+ * @return 스케줄 등록 성공 상태
+ */
@PostMapping("/day")
public ResponseEntity> createDaySchedules(
@Valid @RequestBody CreateDayScheduleRequest createDayScheduleRequest,
@@ -35,7 +44,16 @@ public ResponseEntity> createDaySchedules(
return ApiResponse.onSuccess(SuccessStatus._CREATED_DAY_SCHEDULES);
}
- // 날짜 스케줄 등록 API
+ /**
+ * 날짜 스케줄 등록 API
+ *
+ * 특정 날짜에 대한 스케줄을 등록하는 API입니다.
+ * 인증된 사용자와 비인증 사용자에 따라 스케줄 생성 방식이 다릅니다.
+ *
+ * @param createDateScheduleRequest 날짜 스케줄 생성 요청 객체 (이벤트 ID, 멤버 ID, 날짜 스케줄 목록)
+ * @param authorizationHeader 인증된 유저의 토큰 (선택사항)
+ * @return 스케줄 등록 성공 상태
+ */
@PostMapping("/date")
public ResponseEntity> createDateSchedules(
@Valid @RequestBody CreateDateScheduleRequest createDateScheduleRequest,
@@ -49,7 +67,14 @@ public ResponseEntity> createDateSchedules(
return ApiResponse.onSuccess(SuccessStatus._CREATED_DATE_SCHEDULES);
}
- // 전체 요일 스케줄 조회 API
+ /**
+ * 전체 요일 스케줄 조회 API
+ *
+ * 특정 이벤트에 등록된 모든 요일 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @return 이벤트에 등록된 요일 스케줄 목록
+ */
@GetMapping("/day/{event_id}")
public ResponseEntity>> getAllDaySchedules(
@PathVariable("event_id") String eventId) {
@@ -58,7 +83,15 @@ public ResponseEntity>> getAllDaySched
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DAY_SCHEDULES, perDaySchedulesResponses);
}
- // 개인 요일 스케줄 조회 API (비로그인)
+ /**
+ * 개인 요일 스케줄 조회 API (비로그인)
+ *
+ * 비로그인 사용자의 특정 이벤트에 대한 개인 요일 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @param memberId 조회할 멤버의 ID
+ * @return 멤버의 요일 스케줄
+ */
@GetMapping("/day/{event_id}/{member_id}")
public ResponseEntity> getMemberDaySchedules(
@PathVariable("event_id") String eventId,
@@ -68,7 +101,15 @@ public ResponseEntity> getMemberDaySchedule
return ApiResponse.onSuccess(SuccessStatus._GET_MEMBER_DAY_SCHEDULES, perDaySchedulesResponse);
}
- // 개인 요일 스케줄 조회 API (로그인)
+ /**
+ * 개인 요일 스케줄 조회 API (로그인)
+ *
+ * 인증된 사용자의 특정 이벤트에 대한 개인 요일 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 사용자의 요일 스케줄
+ */
@GetMapping("/day/{event_id}/user")
public ResponseEntity> getUserDaySchedules(
@PathVariable("event_id") String eventId,
@@ -78,7 +119,14 @@ public ResponseEntity> getUserDaySchedules(
return ApiResponse.onSuccess(SuccessStatus._GET_USER_DAY_SCHEDULES, perDaySchedulesResponse);
}
- // 멤버 필터링 요일 스케줄 조회 API
+ /**
+ * 멤버 필터링 요일 스케줄 조회 API
+ *
+ * 멤버 이름을 필터링하여 특정 이벤트의 요일 스케줄을 조회합니다.
+ *
+ * @param getFilteredSchedulesRequest 필터링할 스케줄 요청 객체 (이벤트 ID, 멤버 이름 목록)
+ * @return 필터링된 요일 스케줄 목록
+ */
@GetMapping("/day/action-filtering")
public ResponseEntity>> getFilteredDaySchedules(
@Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {
@@ -87,7 +135,14 @@ public ResponseEntity>> getFilteredDay
return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DAY_SCHEDULES, perDaySchedulesResponses);
}
- // 전체 날짜 스케줄 조회 API
+ /**
+ * 전체 날짜 스케줄 조회 API
+ *
+ * 특정 이벤트에 등록된 모든 날짜 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @return 이벤트에 등록된 날짜 스케줄 목록
+ */
@GetMapping("/date/{event_id}")
public ResponseEntity>> getAllDateSchedules(
@PathVariable("event_id") String eventId) {
@@ -96,7 +151,15 @@ public ResponseEntity>> getAllDateSch
return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DATE_SCHEDULES, perDateSchedulesResponses);
}
- // 개인 날짜 스케줄 조회 API (비로그인)
+ /**
+ * 개인 날짜 스케줄 조회 API (비로그인)
+ *
+ * 비로그인 사용자의 특정 이벤트에 대한 개인 날짜 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @param memberId 조회할 멤버의 ID
+ * @return 멤버의 날짜 스케줄
+ */
@GetMapping("/date/{event_id}/{member_id}")
public ResponseEntity> getMemberDateSchedules(
@PathVariable("event_id") String eventId,
@@ -106,7 +169,15 @@ public ResponseEntity> getMemberDateSchedu
return ApiResponse.onSuccess(SuccessStatus._GET_MEMBER_DATE_SCHEDULES, perDateSchedulesResponse);
}
- // 개인 날짜 스케줄 조회 API (로그인)
+ /**
+ * 개인 날짜 스케줄 조회 API (로그인)
+ *
+ * 인증된 사용자의 특정 이벤트에 대한 개인 날짜 스케줄을 조회합니다.
+ *
+ * @param eventId 조회할 이벤트의 ID
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 사용자의 날짜 스케줄
+ */
@GetMapping("/date/{event_id}/user")
public ResponseEntity> getUserDateSchedules(
@PathVariable("event_id") String eventId,
@@ -116,7 +187,14 @@ public ResponseEntity> getUserDateSchedule
return ApiResponse.onSuccess(SuccessStatus._GET_USER_DATE_SCHEDULES, perDateSchedulesResponse);
}
- // 멤버 필터링 날짜 스케줄 조회 API
+ /**
+ * 멤버 필터링 날짜 스케줄 조회 API
+ *
+ * 멤버 이름을 필터링하여 특정 이벤트의 날짜 스케줄을 조회합니다.
+ *
+ * @param getFilteredSchedulesRequest 필터링할 스케줄 요청 객체 (이벤트 ID, 멤버 이름 목록)
+ * @return 필터링된 날짜 스케줄 목록
+ */
@GetMapping("/date/action-filtering")
public ResponseEntity>> getFilteredDateSchedules(
@Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) {
@@ -124,4 +202,4 @@ public ResponseEntity>> getFilteredDa
List perDateSchedulesResponses = scheduleService.getFilteredDateSchedules(getFilteredSchedulesRequest);
return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DATE_SCHEDULES, perDateSchedulesResponses);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/TokenController.java b/src/main/java/side/onetime/controller/TokenController.java
index 471d278..f0355f4 100644
--- a/src/main/java/side/onetime/controller/TokenController.java
+++ b/src/main/java/side/onetime/controller/TokenController.java
@@ -19,7 +19,15 @@
public class TokenController {
private final TokenService tokenService;
- // 액세스 토큰 재발행 API
+ /**
+ * 액세스 토큰 재발행 API
+ *
+ * 이 API는 유효한 리프레쉬 토큰을 제공받아 새 액세스 토큰과 리프레쉬 토큰을 재발행합니다.
+ * 리프레쉬 토큰의 유효성을 검증하고, 인증 정보에 따라 토큰을 갱신합니다.
+ *
+ * @param reissueAccessTokenRequest 리프레쉬 토큰을 포함한 요청 객체
+ * @return 재발행된 액세스 토큰과 리프레쉬 토큰을 포함하는 응답 객체
+ */
@PostMapping("/action-reissue")
public ResponseEntity> reissueToken(
@Valid @RequestBody ReissueTokenRequest reissueAccessTokenRequest) {
diff --git a/src/main/java/side/onetime/controller/UrlController.java b/src/main/java/side/onetime/controller/UrlController.java
index 2425283..4ff0557 100644
--- a/src/main/java/side/onetime/controller/UrlController.java
+++ b/src/main/java/side/onetime/controller/UrlController.java
@@ -21,16 +21,32 @@
public class UrlController {
private final UrlService urlService;
- // 원본 -> 단축 URL API
+ /**
+ * 원본 URL을 단축 URL로 변환하는 API
+ *
+ * 이 API는 제공된 원본 URL을 단축 URL로 변환합니다.
+ * 주어진 URL에서 이벤트 ID를 추출하고, 해당 이벤트가 존재할 경우에만 단축 URL을 생성하여 반환합니다.
+ *
+ * @param convertToShortenUrlRequest 원본 URL을 포함한 요청 객체
+ * @return 변환된 단축 URL을 포함하는 응답 객체
+ */
@PostMapping("/action-shorten")
public ResponseEntity> convertToShortenUrl(
- @Valid @RequestBody ConvertToShortenUrlRequest covertToShortenUrlRequest) {
+ @Valid @RequestBody ConvertToShortenUrlRequest convertToShortenUrlRequest) {
- ConvertToShortenUrlResponse convertToShortenUrlResponse = urlService.convertToShortenUrl(covertToShortenUrlRequest);
+ ConvertToShortenUrlResponse convertToShortenUrlResponse = urlService.convertToShortenUrl(convertToShortenUrlRequest);
return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_SHORTEN_URL, convertToShortenUrlResponse);
}
- // 단축 -> 원본 URL API
+ /**
+ * 단축 URL을 원본 URL로 복원하는 API
+ *
+ * 이 API는 단축된 URL을 원래의 URL로 복원합니다.
+ * 복원된 URL에서 이벤트 ID를 추출하여, 해당 이벤트가 존재하는지 확인 후 원본 URL을 반환합니다.
+ *
+ * @param convertToOriginalUrlRequest 단축 URL을 포함한 요청 객체
+ * @return 복원된 원본 URL을 포함하는 응답 객체
+ */
@PostMapping("/action-original")
public ResponseEntity> convertToOriginalUrl(
@Valid @RequestBody ConvertToOriginalUrlRequest convertToOriginalUrlRequest) {
@@ -38,4 +54,4 @@ public ResponseEntity> convertToOrigin
ConvertToOriginalUrlResponse convertToOriginalUrlResponse = urlService.convertToOriginalUrl(convertToOriginalUrlRequest);
return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_ORIGINAL_URL, convertToOriginalUrlResponse);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/controller/UserController.java b/src/main/java/side/onetime/controller/UserController.java
index 2a135aa..93dad6d 100644
--- a/src/main/java/side/onetime/controller/UserController.java
+++ b/src/main/java/side/onetime/controller/UserController.java
@@ -18,7 +18,15 @@
public class UserController {
private final UserService userService;
- // 유저 온보딩 API
+ /**
+ * 유저 온보딩 API
+ *
+ * 회원가입 이후, 유저의 닉네임을 설정하고 온보딩을 완료하는 API입니다.
+ * 주어진 레지스터 토큰을 통해 사용자 정보를 확인한 후, 액세스 토큰과 리프레쉬 토큰을 발급하여 반환합니다.
+ *
+ * @param onboardUserRequest 유저의 레지스터 토큰과 닉네임 정보를 포함하는 요청 객체
+ * @return 발급된 액세스 토큰과 리프레쉬 토큰을 포함하는 응답 객체
+ */
@PostMapping("/onboarding")
public ResponseEntity> onboardUser(
@Valid @RequestBody OnboardUserRequest onboardUserRequest) {
@@ -27,7 +35,14 @@ public ResponseEntity> onboardUser(
return ApiResponse.onSuccess(SuccessStatus._ONBOARD_USER, onboardUserResponse);
}
- // 유저 정보 조회 API
+ /**
+ * 유저 정보 조회 API
+ *
+ * 로그인한 유저의 닉네임과 이메일 정보를 조회합니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 유저의 닉네임과 이메일을 포함한 응답 객체
+ */
@GetMapping("/profile")
public ResponseEntity> getUserProfile(
@RequestHeader("Authorization") String authorizationHeader) {
@@ -36,7 +51,15 @@ public ResponseEntity> getUserProfile(
return ApiResponse.onSuccess(SuccessStatus._GET_USER_PROFILE, getUserProfileResponse);
}
- // 유저 정보 수정 API
+ /**
+ * 유저 정보 수정 API
+ *
+ * 유저의 닉네임을 수정하는 API입니다. 수정된 닉네임은 최대 길이 제한을 받습니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @param updateUserProfileRequest 수정할 닉네임을 포함하는 요청 객체
+ * @return 성공 상태 응답 객체
+ */
@PatchMapping("/profile/action-update")
public ResponseEntity> updateUserProfile(
@RequestHeader("Authorization") String authorizationHeader,
@@ -46,7 +69,14 @@ public ResponseEntity> updateUserProfile(
return ApiResponse.onSuccess(SuccessStatus._UPDATE_USER_PROFILE);
}
- // 유저 서비스 탈퇴 API
+ /**
+ * 유저 서비스 탈퇴 API
+ *
+ * 유저의 계정을 삭제하여 서비스에서 탈퇴하는 API입니다.
+ *
+ * @param authorizationHeader 인증된 유저의 토큰
+ * @return 성공 상태 응답 객체
+ */
@PostMapping("/action-withdraw")
public ResponseEntity> withdrawService(
@RequestHeader("Authorization") String authorizationHeader) {
diff --git a/src/main/java/side/onetime/domain/Event.java b/src/main/java/side/onetime/domain/Event.java
index b14ed97..24793c7 100644
--- a/src/main/java/side/onetime/domain/Event.java
+++ b/src/main/java/side/onetime/domain/Event.java
@@ -55,4 +55,8 @@ public Event(UUID eventId, String title, String startTime, String endTime, Categ
this.endTime = endTime;
this.category = category;
}
+
+ public void updateTitle(String title) {
+ this.title = title;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java b/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java
new file mode 100644
index 0000000..6cf98e6
--- /dev/null
+++ b/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java
@@ -0,0 +1,13 @@
+package side.onetime.dto.event.request;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import jakarta.validation.constraints.NotBlank;
+
+@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public record ModifyUserCreatedEventTitleRequest(
+ @NotBlank(message = "변경할 제목은 필수 값입니다.") String title
+) {
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/global/common/status/ErrorStatus.java b/src/main/java/side/onetime/global/common/status/ErrorStatus.java
index 5f0b9f8..0c3a7a0 100644
--- a/src/main/java/side/onetime/global/common/status/ErrorStatus.java
+++ b/src/main/java/side/onetime/global/common/status/ErrorStatus.java
@@ -17,6 +17,7 @@ public enum ErrorStatus implements BaseErrorCode {
_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "405", "허용되지 않은 요청 메소드입니다."),
_UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "415", "지원되지 않는 미디어 타입입니다."),
_NOT_FOUND_HANDLER(HttpStatus.NOT_FOUND, "404", "해당 경로에 대한 핸들러를 찾을 수 없습니다."),
+ _FAILED_TRANSLATE_SWAGGER(HttpStatus.INTERNAL_SERVER_ERROR, "500", "Rest Docs로 생성된 json파일을 통한 스웨거 변환에 실패하였습니다.")
;
private final HttpStatus httpStatus;
diff --git a/src/main/java/side/onetime/global/common/status/SuccessStatus.java b/src/main/java/side/onetime/global/common/status/SuccessStatus.java
index 9fefd12..a649526 100644
--- a/src/main/java/side/onetime/global/common/status/SuccessStatus.java
+++ b/src/main/java/side/onetime/global/common/status/SuccessStatus.java
@@ -19,6 +19,7 @@ public enum SuccessStatus implements BaseCode {
_GET_MOST_POSSIBLE_TIME(HttpStatus.OK, "200", "가장 많이 되는 시간 조회에 성공했습니다."),
_GET_USER_PARTICIPATED_EVENTS(HttpStatus.OK, "200", "유저 참여 이벤트 목록 조회에 성공했습니다."),
_REMOVE_USER_CREATED_EVENT(HttpStatus.OK, "200", "유저가 생성한 이벤트 삭제에 성공했습니다."),
+ _MODIFY_USER_CREATED_EVENT_TITLE(HttpStatus.OK, "200", "유저가 생성한 이벤트 제목 수정에 성공했습니다."),
// Member
_REGISTER_MEMBER(HttpStatus.CREATED, "201", "멤버 등록에 성공했습니다."),
_LOGIN_MEMBER(HttpStatus.OK, "200", "멤버 로그인에 성공했습니다."),
@@ -30,7 +31,7 @@ public enum SuccessStatus implements BaseCode {
_GET_MEMBER_DAY_SCHEDULES(HttpStatus.OK, "200", "개인(비로그인) 요일 스케줄 조회에 성공했습니다."),
_GET_USER_DAY_SCHEDULES(HttpStatus.OK, "200", "개인(로그인) 요일 스케줄 조회에 성공했습니다."),
_GET_ALL_DATE_SCHEDULES(HttpStatus.OK, "200", "전체 날짜 스케줄 조회에 성공했습니다."),
- _GET_MEMBER_DATE_SCHEDULES(HttpStatus.OK, "200", "개인(비로그인)날짜 스케줄 조회에 성공했습니다."),
+ _GET_MEMBER_DATE_SCHEDULES(HttpStatus.OK, "200", "개인(비로그인) 날짜 스케줄 조회에 성공했습니다."),
_GET_USER_DATE_SCHEDULES(HttpStatus.OK, "200", "개인(로그인) 날짜 스케줄 조회에 성공했습니다."),
_GET_FILTERED_DAY_SCHEDULES(HttpStatus.OK, "200", "멤버 필터링 요일 스케줄 조회에 성공했습니다."),
_GET_FILTERED_DATE_SCHEDULES(HttpStatus.OK, "200", "멤버 필터링 날짜 스케줄 조회에 성공했습니다."),
diff --git a/src/main/java/side/onetime/global/config/SecurityConfig.java b/src/main/java/side/onetime/global/config/SecurityConfig.java
index be505a1..586bfbf 100644
--- a/src/main/java/side/onetime/global/config/SecurityConfig.java
+++ b/src/main/java/side/onetime/global/config/SecurityConfig.java
@@ -21,24 +21,37 @@
@Configuration
@EnableWebSecurity
public class SecurityConfig {
+
private final OAuthLoginSuccessHandler oAuthLoginSuccessHandler;
private final OAuthLoginFailureHandler oAuthLoginFailureHandler;
+ private static final String[] SWAGGER_URLS = {
+ "/swagger-ui/**", "/v3/api-docs/**"
+ };
+
+ private static final String[] ALLOWED_ORIGINS = {
+ "http://localhost:5173",
+ "https://onetime-test.vercel.app",
+ "https://www.onetime-test.vercel.app",
+ "https://onetime-with-members.com",
+ "https://www.onetime-with-members.com",
+ "https://1-ti.me",
+ "https://www.1-ti.me",
+ "https://noonsachin.com",
+ "https://www.noonsachin.com",
+ "https://onetime-test.store",
+ "https://www.onetime-test.store",
+ };
+
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
- config.setAllowedOrigins(Arrays.asList(
- "http://localhost:5173",
- "https://onetime-test.vercel.app",
- "https://www.onetime-test.vercel.app",
- "https://onetime-with-members.com",
- "https://www.onetime-with-members.com",
- "https://1-ti.me",
- "https://www.1-ti.me"
- ));
+ config.setAllowedOrigins(Arrays.asList(ALLOWED_ORIGINS));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(true);
+ config.setExposedHeaders(Arrays.asList("Authorization", "Set-Cookie"));
+ config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
@@ -51,14 +64,14 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
.httpBasic(HttpBasicConfigurer::disable)
.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(authorize ->
- authorize
- .requestMatchers("/**").permitAll()
+ .authorizeHttpRequests(authorize -> authorize
+ .requestMatchers(SWAGGER_URLS).permitAll()
+ .requestMatchers("/**").permitAll() // 추후 변경 필요
+ .anyRequest().authenticated()
)
- .oauth2Login(oauth -> // OAuth2 로그인 기능에 대한 여러 설정의 진입점
- oauth
- .successHandler(oAuthLoginSuccessHandler) // 로그인 성공 시 핸들러
- .failureHandler(oAuthLoginFailureHandler) // 로그인 실패 시 핸들러
+ .oauth2Login(oauth -> oauth
+ .successHandler(oAuthLoginSuccessHandler) // OAuth 로그인 성공 핸들러
+ .failureHandler(oAuthLoginFailureHandler) // OAuth 로그인 실패 핸들러
);
return httpSecurity.build();
diff --git a/src/main/java/side/onetime/global/config/SwaggerConfig.java b/src/main/java/side/onetime/global/config/SwaggerConfig.java
new file mode 100644
index 0000000..f8de420
--- /dev/null
+++ b/src/main/java/side/onetime/global/config/SwaggerConfig.java
@@ -0,0 +1,73 @@
+package side.onetime.global.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.oas.models.servers.Server;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import side.onetime.exception.CustomException;
+import side.onetime.global.common.status.ErrorStatus;
+
+import java.io.InputStream;
+import java.util.List;
+
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ OpenAPI openAPI = new OpenAPI()
+ .info(new Info()
+ .title("OneTime API Documentation")
+ .version("1.3.0")
+ .description("Spring REST Docs with Swagger UI.")
+ .contact(new Contact()
+ .name("Sangho Han")
+ .url("https://github.com/bbbang105")
+ .email("hchsa77@gmail.com"))
+ )
+ .servers(List.of(
+ new Server().url("http://localhost:8090").description("로컬 서버"),
+ new Server().url("https://onetime-test.store").description("테스트 서버")
+ ));
+
+ // REST Docs에서 생성한 open-api-3.0.1.json 파일 읽어오기
+ try {
+ ClassPathResource resource = new ClassPathResource("static/docs/open-api-3.0.1.json");
+ InputStream inputStream = resource.getInputStream();
+ ObjectMapper mapper = new ObjectMapper();
+
+ // open-api-3.0.1.json 파일을 OpenAPI 객체로 매핑
+ OpenAPI restDocsOpenAPI = mapper.readValue(inputStream, OpenAPI.class);
+
+ // REST Docs에서 생성한 Paths 정보 병합
+ Paths paths = restDocsOpenAPI.getPaths();
+ openAPI.setPaths(paths);
+
+ openAPI.components(restDocsOpenAPI.getComponents());
+
+ } catch (Exception e) {
+ throw new CustomException(ErrorStatus._FAILED_TRANSLATE_SWAGGER);
+ }
+
+ // 액세스 토큰
+ SecurityScheme apiKey = new SecurityScheme()
+ .type(SecurityScheme.Type.APIKEY)
+ .in(SecurityScheme.In.HEADER)
+ .name("Authorization");
+ SecurityRequirement securityRequirement = new SecurityRequirement()
+ .addList("Bearer Token");
+
+ openAPI.components(new Components().addSecuritySchemes("Bearer Token", apiKey))
+ .addSecurityItem(securityRequirement);
+
+ return openAPI;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/side/onetime/service/EventService.java b/src/main/java/side/onetime/service/EventService.java
index c51fab1..639c4c0 100644
--- a/src/main/java/side/onetime/service/EventService.java
+++ b/src/main/java/side/onetime/service/EventService.java
@@ -7,6 +7,7 @@
import side.onetime.domain.enums.Category;
import side.onetime.domain.enums.EventStatus;
import side.onetime.dto.event.request.CreateEventRequest;
+import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest;
import side.onetime.dto.event.response.*;
import side.onetime.exception.CustomException;
import side.onetime.exception.status.EventErrorStatus;
@@ -228,17 +229,22 @@ private List buildMostPossibleTimes(Map mostPossibleTimes = new ArrayList<>();
GetMostPossibleTime previousTime = null;
+ boolean stopFlag = false;
for (Map.Entry> entry : scheduleToNamesMap.entrySet()) {
Schedule schedule = entry.getKey();
List curNames = entry.getValue();
if (curNames.size() == mostPossibleCnt) {
- // 이전 시간대와 병합 가능한 경우
if (canMergeWithPrevious(previousTime, schedule, curNames, category)) {
- // 종료 시간을 더해 업데이트
+ // 이전 시간대와 병합 가능한 경우
previousTime = previousTime.updateEndTime(schedule.getTime());
- mostPossibleTimes.set(mostPossibleTimes.size() - 1, previousTime);
+ mostPossibleTimes.set(mostPossibleTimes.size() - 1, previousTime); // 종료 시간을 더해 업데이트
} else {
+ // 새로운 시간대를 추가하는 경우
+ if (mostPossibleTimes.size() == MAX_MOST_POSSIBLE_TIMES_SIZE) {
+ // 6개를 찾았을 시 종료
+ stopFlag = true;
+ }
List impossibleNames = allMembersName.stream()
.filter(name -> !curNames.contains(name))
.toList();
@@ -249,8 +255,7 @@ private List buildMostPossibleTimes(Map getUserParticipatedEvents(String
// 유저가 생성한 이벤트 삭제 메서드
@Transactional
public void removeUserCreatedEvent(String authorizationHeader, String eventId) {
+ EventParticipation eventParticipation = verifyUserIsEventCreator(authorizationHeader, eventId);
+ eventRepository.deleteEvent(eventParticipation.getEvent());
+ }
+
+ // 유저가 생성한 이벤트 제목 수정 메서드
+ @Transactional
+ public void modifyUserCreatedEventTitle(String authorizationHeader, String eventId, ModifyUserCreatedEventTitleRequest modifyUserCreatedEventTitleRequest) {
+ EventParticipation eventParticipation = verifyUserIsEventCreator(authorizationHeader, eventId);
+ eventParticipation.getEvent().updateTitle(modifyUserCreatedEventTitleRequest.title());
+ }
+
+ // 유저가 이벤트의 생성자인지 검증하는 메서드
+ private EventParticipation verifyUserIsEventCreator(String authorizationHeader, String eventId) {
User user = jwtUtil.getUserFromHeader(authorizationHeader);
Event event = eventRepository.findByEventId(UUID.fromString(eventId))
.orElseThrow(() -> new CustomException(EventErrorStatus._NOT_FOUND_EVENT));
@@ -314,10 +332,9 @@ public void removeUserCreatedEvent(String authorizationHeader, String eventId) {
throw new CustomException(EventParticipationErrorStatus._NOT_FOUND_EVENT_PARTICIPATION);
}
if (!EventStatus.CREATOR.equals(eventParticipation.getEventStatus())) {
- // 해당 이벤트의 생성자가 아닌 경우
throw new CustomException(EventParticipationErrorStatus._IS_NOT_USERS_CREATED_EVENT_PARTICIPATION);
}
- eventRepository.deleteEvent(event);
+ return eventParticipation;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 676a962..1f64809 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -1,5 +1,5 @@
server:
- port: ${SERVER_PORT}
+ port: 8090
spring:
datasource:
@@ -89,4 +89,13 @@ jwt:
expiration-time: ${REGISTER_TOKEN_EXPIRATION_TIME}
scheduling:
- cron: ${CRON}
\ No newline at end of file
+ cron: ${CRON}
+
+springdoc:
+ swagger-ui:
+ path: /swagger-ui.html
+ tags-sorter: alpha
+ operations-sorter: alpha
+ api-docs:
+ path: /v3/api-docs
+ show-actuator: true
\ No newline at end of file
diff --git a/src/main/resources/static/docs/open-api-3.0.1.json b/src/main/resources/static/docs/open-api-3.0.1.json
new file mode 100644
index 0000000..9edf093
--- /dev/null
+++ b/src/main/resources/static/docs/open-api-3.0.1.json
@@ -0,0 +1,16 @@
+{
+ "openapi" : "3.0.1",
+ "info" : {
+ "title" : "OneTime API Documentation",
+ "description" : "Spring REST Docs with Swagger UI.",
+ "version" : "0.0.1"
+ },
+ "servers" : [ {
+ "url" : "https://onetime-test.store"
+ } ],
+ "tags" : [ ],
+ "paths" : { },
+ "components" : {
+ "schemas" : { }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/configuration/ControllerTestConfig.java b/src/test/java/side/onetime/configuration/ControllerTestConfig.java
new file mode 100644
index 0000000..494f4d0
--- /dev/null
+++ b/src/test/java/side/onetime/configuration/ControllerTestConfig.java
@@ -0,0 +1,37 @@
+package side.onetime.configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.filter.CharacterEncodingFilter;
+
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+
+@AutoConfigureMockMvc
+@AutoConfigureRestDocs
+@ExtendWith({RestDocumentationExtension.class})
+public abstract class ControllerTestConfig {
+ @Autowired
+ protected WebApplicationContext context;
+ @Autowired
+ protected ObjectMapper objectMapper;
+ protected MockMvc mockMvc;
+
+ @BeforeEach
+ void setUp(final RestDocumentationContextProvider restDocumentation) {
+ mockMvc = MockMvcBuilders.webAppContextSetup(context)
+ .apply(documentationConfiguration(restDocumentation))
+ .addFilters(new CharacterEncodingFilter("UTF-8", true))
+ .alwaysDo(print())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/event/EventControllerTest.java b/src/test/java/side/onetime/event/EventControllerTest.java
new file mode 100644
index 0000000..0dd9f82
--- /dev/null
+++ b/src/test/java/side/onetime/event/EventControllerTest.java
@@ -0,0 +1,439 @@
+package side.onetime.event;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.EventController;
+import side.onetime.domain.enums.Category;
+import side.onetime.domain.enums.EventStatus;
+import side.onetime.dto.event.request.CreateEventRequest;
+import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest;
+import side.onetime.dto.event.response.*;
+import side.onetime.service.EventService;
+import side.onetime.util.JwtUtil;
+
+import java.util.List;
+import java.util.UUID;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(EventController.class)
+public class EventControllerTest extends ControllerTestConfig {
+
+ @MockBean
+ private EventService eventService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ @Test
+ @DisplayName("이벤트를 생성한다. (토큰 유무에 따라 로그인/비로그인 구분)")
+ public void createEventForAnonymousUser() throws Exception {
+ // given
+ UUID eventId = UUID.randomUUID();
+ CreateEventResponse response = new CreateEventResponse(eventId);
+ Mockito.when(eventService.createEventForAnonymousUser(any(CreateEventRequest.class)))
+ .thenReturn(response);
+
+ CreateEventRequest request = new CreateEventRequest(
+ "Sample Event",
+ "10:00",
+ "12:00",
+ Category.DATE,
+ List.of("2024.11.13")
+ );
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/events")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("이벤트 생성에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.event_id").value(eventId.toString()))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/create",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("이벤트를 생성한다.(토큰 유무에 따라 로그인/비로그인 구분)")
+ .requestFields(
+ fieldWithPath("title").type(JsonFieldType.STRING).description("이벤트 제목"),
+ fieldWithPath("start_time").type(JsonFieldType.STRING).description("이벤트 시작 시간"),
+ fieldWithPath("end_time").type(JsonFieldType.STRING).description("이벤트 종료 시간"),
+ fieldWithPath("category").type(JsonFieldType.STRING).description("이벤트 카테고리"),
+ fieldWithPath("ranges").type(JsonFieldType.ARRAY).description("이벤트 날짜 또는 요일 범위")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.event_id").type(JsonFieldType.STRING).description("생성된 이벤트의 UUID (형식: UUID)")
+ )
+ .requestSchema(Schema.schema("CreateEventRequestSchema"))
+ .responseSchema(Schema.schema("CreateEventResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("이벤트를 조회한다.")
+ public void getEvent() throws Exception {
+ // given
+ UUID eventId = UUID.randomUUID();
+ GetEventResponse response = new GetEventResponse(
+ "Sample Event",
+ "10:00",
+ "12:00",
+ Category.DATE,
+ List.of("2024.11.13"),
+ EventStatus.CREATOR
+ );
+
+ Mockito.when(eventService.getEvent(eventId.toString(), null))
+ .thenReturn(response);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/events/{event_id}", eventId)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("이벤트 조회에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.title").value("Sample Event"))
+ .andExpect(jsonPath("$.payload.start_time").value("10:00"))
+ .andExpect(jsonPath("$.payload.end_time").value("12:00"))
+ .andExpect(jsonPath("$.payload.category").value("DATE"))
+ .andExpect(jsonPath("$.payload.ranges[0]").value("2024.11.13"))
+ .andExpect(jsonPath("$.payload.event_status").value("CREATOR"))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/get",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("이벤트를 조회한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("조회할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.title").type(JsonFieldType.STRING).description("이벤트 제목"),
+ fieldWithPath("payload.start_time").type(JsonFieldType.STRING).description("이벤트 시작 시간"),
+ fieldWithPath("payload.end_time").type(JsonFieldType.STRING).description("이벤트 종료 시간"),
+ fieldWithPath("payload.category").type(JsonFieldType.STRING).description("이벤트 카테고리"),
+ fieldWithPath("payload.ranges").type(JsonFieldType.ARRAY).description("이벤트 날짜 또는 요일 범위"),
+ fieldWithPath("payload.event_status").type(JsonFieldType.STRING).description("이벤트 상태 (로그인 유저만 반환)")
+ )
+ .responseSchema(Schema.schema("GetEventResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("이벤트 참여자 목록을 조회한다.")
+ public void getParticipants() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ GetParticipantsResponse response = new GetParticipantsResponse(List.of("Member1", "User1", "Member2", "User2"));
+
+ Mockito.when(eventService.getParticipants(anyString()))
+ .thenReturn(response);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/events/{event_id}/participants", eventId)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("참여자 조회에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.names[0]").value("Member1"))
+ .andExpect(jsonPath("$.payload.names[1]").value("User1"))
+ .andExpect(jsonPath("$.payload.names[2]").value("Member2"))
+ .andExpect(jsonPath("$.payload.names[3]").value("User2"))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/get-participants",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("이벤트 참여자 목록을 조회한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("조회할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.names").type(JsonFieldType.ARRAY).description("참여자 이름 목록")
+ )
+ .responseSchema(Schema.schema("GetParticipantsResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("가장 많이 되는 시간을 조회한다.")
+ public void getMostPossibleTime() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ List response = List.of(
+ new GetMostPossibleTime("2024.11.13", "10:00", "10:30", 5, List.of("User1", "User2"), List.of("User3")),
+ new GetMostPossibleTime("2024.11.13", "11:00", "11:30", 4, List.of("User1", "User3"), List.of("User2"))
+ );
+
+ Mockito.when(eventService.getMostPossibleTime(anyString())).thenReturn(response);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/events/{event_id}/most", eventId)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("가장 많이 되는 시간 조회에 성공했습니다."))
+ .andExpect(jsonPath("$.payload[0].time_point").value("2024.11.13"))
+ .andExpect(jsonPath("$.payload[0].start_time").value("10:00"))
+ .andExpect(jsonPath("$.payload[0].end_time").value("10:30"))
+ .andExpect(jsonPath("$.payload[0].possible_count").value(5))
+ .andExpect(jsonPath("$.payload[0].possible_names[0]").value("User1"))
+ .andExpect(jsonPath("$.payload[0].possible_names[1]").value("User2"))
+ .andExpect(jsonPath("$.payload[0].impossible_names[0]").value("User3"))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/get-most-possible-time",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("가장 많이 되는 시간을 조회한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("조회할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.ARRAY).description("가장 많이 되는 시간 목록"),
+ fieldWithPath("payload[].time_point").type(JsonFieldType.STRING).description("날짜 또는 요일"),
+ fieldWithPath("payload[].start_time").type(JsonFieldType.STRING).description("시작 시간"),
+ fieldWithPath("payload[].end_time").type(JsonFieldType.STRING).description("종료 시간"),
+ fieldWithPath("payload[].possible_count").type(JsonFieldType.NUMBER).description("가능한 참여자 수"),
+ fieldWithPath("payload[].possible_names").type(JsonFieldType.ARRAY).description("가능한 참여자 이름 목록"),
+ fieldWithPath("payload[].impossible_names").type(JsonFieldType.ARRAY).description("참여 불가능한 이름 목록")
+ )
+ .responseSchema(Schema.schema("GetMostPossibleTimeResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저 참여 이벤트 목록을 조회한다.")
+ public void getUserParticipatedEvents() throws Exception {
+ // given
+ List response = List.of(
+ new GetUserParticipatedEventsResponse(
+ UUID.randomUUID(),
+ Category.DATE,
+ "Sample Event",
+ "2024.11.13",
+ 10,
+ EventStatus.CREATOR,
+ List.of(
+ new GetMostPossibleTime("2024.11.13", "10:00", "10:30", 5, List.of("User1", "User2"), List.of("User3"))
+ )
+ )
+ );
+
+ Mockito.when(eventService.getUserParticipatedEvents(anyString())).thenReturn(response);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/events/user/all")
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저 참여 이벤트 목록 조회에 성공했습니다."))
+ .andExpect(jsonPath("$.payload[0].event_id").exists())
+ .andExpect(jsonPath("$.payload[0].title").value("Sample Event"))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/get-user-participated-events",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("유저가 참여한 이벤트 목록을 조회한다.")
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.ARRAY).description("참여 이벤트 목록"),
+ fieldWithPath("payload[].event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("payload[].category").type(JsonFieldType.STRING).description("이벤트 카테고리"),
+ fieldWithPath("payload[].title").type(JsonFieldType.STRING).description("이벤트 제목"),
+ fieldWithPath("payload[].created_date").type(JsonFieldType.STRING).description("이벤트 생성일"),
+ fieldWithPath("payload[].participant_count").type(JsonFieldType.NUMBER).description("참여자 수"),
+ fieldWithPath("payload[].event_status").type(JsonFieldType.STRING).description("이벤트 참여 상태"),
+ fieldWithPath("payload[].most_possible_times").type(JsonFieldType.ARRAY).description("가장 많이 가능한 시간대"),
+ fieldWithPath("payload[].most_possible_times[].time_point").type(JsonFieldType.STRING).description("날짜 또는 요일"),
+ fieldWithPath("payload[].most_possible_times[].start_time").type(JsonFieldType.STRING).description("시작 시간"),
+ fieldWithPath("payload[].most_possible_times[].end_time").type(JsonFieldType.STRING).description("종료 시간"),
+ fieldWithPath("payload[].most_possible_times[].possible_count").type(JsonFieldType.NUMBER).description("가능한 참여자 수"),
+ fieldWithPath("payload[].most_possible_times[].possible_names").type(JsonFieldType.ARRAY).description("참여 가능한 유저 이름 목록"),
+ fieldWithPath("payload[].most_possible_times[].impossible_names").type(JsonFieldType.ARRAY).description("참여 불가능한 유저 이름 목록")
+ )
+ .responseSchema(Schema.schema("GetUserParticipatedEventsResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저가 생성한 이벤트를 삭제한다.")
+ public void removeUserCreatedEvent() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ Mockito.doNothing().when(eventService).removeUserCreatedEvent(anyString(), anyString());
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/events/{event_id}", eventId)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저가 생성한 이벤트 삭제에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/remove-user-created-event",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("유저가 생성한 이벤트를 삭제한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("삭제할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .responseSchema(Schema.schema("RemoveUserCreatedEventResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저가 생성한 이벤트 제목을 수정한다.")
+ public void modifyUserCreatedEventTitle() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ ModifyUserCreatedEventTitleRequest request = new ModifyUserCreatedEventTitleRequest("수정할 이벤트 제목");
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ Mockito.doNothing().when(eventService).modifyUserCreatedEventTitle(anyString(), anyString(), any(ModifyUserCreatedEventTitleRequest.class));
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.put("/api/v1/events/{event_id}", eventId)
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestContent)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저가 생성한 이벤트 제목 수정에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("event/modify-user-created-event-title",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Event API")
+ .description("유저가 생성한 이벤트 제목을 수정한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("수정할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .requestFields(
+ fieldWithPath("title").type(JsonFieldType.STRING).description("새로운 이벤트 제목")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .responseSchema(Schema.schema("ModifyUserCreatedEventTitleResponseSchema"))
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/fixed/FixedControllerTest.java b/src/test/java/side/onetime/fixed/FixedControllerTest.java
new file mode 100644
index 0000000..9f50398
--- /dev/null
+++ b/src/test/java/side/onetime/fixed/FixedControllerTest.java
@@ -0,0 +1,309 @@
+package side.onetime.fixed;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.FixedController;
+import side.onetime.dto.fixed.request.CreateFixedEventRequest;
+import side.onetime.dto.fixed.request.ModifyFixedEventRequest;
+import side.onetime.dto.fixed.response.FixedEventByDayResponse;
+import side.onetime.dto.fixed.response.FixedEventDetailResponse;
+import side.onetime.dto.fixed.response.FixedEventResponse;
+import side.onetime.dto.fixed.response.FixedScheduleResponse;
+import side.onetime.service.FixedEventService;
+import side.onetime.service.FixedScheduleService;
+import side.onetime.util.JwtUtil;
+
+import java.util.List;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(FixedController.class)
+public class FixedControllerTest extends ControllerTestConfig {
+
+ @MockBean
+ private FixedEventService fixedEventService;
+
+ @MockBean
+ private FixedScheduleService fixedScheduleService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ private final String authorizationHeader = "Bearer token";
+
+ @Test
+ @DisplayName("고정 스케줄을 등록한다.")
+ public void createFixedEvent() throws Exception {
+ // given
+ CreateFixedEventRequest request = new CreateFixedEventRequest("고정 이벤트", List.of(new FixedScheduleResponse("월", List.of("09:00", "09:30"))));
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/fixed-schedules")
+ .header("Authorization", authorizationHeader)
+ .content(objectMapper.writeValueAsString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("고정 스케줄 등록에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/create",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("고정 스케줄을 등록한다.")
+ .requestFields(
+ fieldWithPath("title").type(JsonFieldType.STRING).description("스케줄 이름"),
+ fieldWithPath("schedules").type(JsonFieldType.ARRAY).description("고정 스케줄 목록"),
+ fieldWithPath("schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("schedules[].times").type(JsonFieldType.ARRAY).description("시간 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("전체 고정 스케줄을 조회한다.")
+ public void getAllFixedSchedules() throws Exception {
+ // given
+ List responses = List.of(
+ new FixedEventResponse(1L, "09:00", "10:00", List.of(new FixedScheduleResponse("월", List.of("09:00", "09:30")))),
+ new FixedEventResponse(2L, "09:00", "10:00", List.of(new FixedScheduleResponse("화", List.of("09:00", "09:30"))))
+ );
+ Mockito.when(fixedScheduleService.getAllFixedSchedules(authorizationHeader)).thenReturn(responses);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/fixed-schedules")
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("전체 고정 스케줄 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/getAll",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("전체 고정 스케줄을 조회한다.")
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].id").type(JsonFieldType.NUMBER).description("고정 스케줄 ID"),
+ fieldWithPath("payload[].start_time").type(JsonFieldType.STRING).description("시작 시간"),
+ fieldWithPath("payload[].end_time").type(JsonFieldType.STRING).description("종료 시간"),
+ fieldWithPath("payload[].schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload[].schedules[].times[]").type(JsonFieldType.ARRAY).description("시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("특정 고정 스케줄을 상세 조회한다.")
+ public void getFixedScheduleDetail() throws Exception {
+ // given
+ Long fixedEventId = 1L;
+ FixedEventDetailResponse response = new FixedEventDetailResponse("고정 이벤트", "09:00", "10:00", List.of(new FixedScheduleResponse("월", List.of("09:00", "09:30"))));
+ Mockito.when(fixedScheduleService.getFixedScheduleDetail(authorizationHeader, fixedEventId)).thenReturn(response);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/fixed-schedules/{id}", fixedEventId)
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("특정 고정 스케줄 상세 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/getDetail",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("특정 고정 스케줄을 상세 조회한다.")
+ .pathParameters(
+ parameterWithName("id").description("고정 스케줄 ID [예시 : 1 (NUMBER Type)]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.title").type(JsonFieldType.STRING).description("고정 스케줄 제목"),
+ fieldWithPath("payload.start_time").type(JsonFieldType.STRING).description("시작 시간"),
+ fieldWithPath("payload.end_time").type(JsonFieldType.STRING).description("종료 시간"),
+ fieldWithPath("payload.schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload.schedules[].times[]").type(JsonFieldType.ARRAY).description("시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("특정 고정 스케줄을 수정한다.")
+ public void modifyFixedEvent() throws Exception {
+ // given
+ Long fixedEventId = 1L;
+ ModifyFixedEventRequest request = new ModifyFixedEventRequest("수정된 고정 스케줄", List.of(new FixedScheduleResponse("화", List.of("10:00", "11:00"))));
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.patch("/api/v1/fixed-schedules/{id}", fixedEventId)
+ .header("Authorization", authorizationHeader)
+ .content(objectMapper.writeValueAsString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("고정 스케줄 수정에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/modify",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("특정 고정 스케줄을 수정한다.")
+ .pathParameters(
+ parameterWithName("id").description("고정 스케줄 ID [예시 : 1 (NUMBER Type)]")
+ )
+ .requestFields(
+ fieldWithPath("title").type(JsonFieldType.STRING).description("수정된 스케줄 이름"),
+ fieldWithPath("schedules").type(JsonFieldType.ARRAY).description("수정된 고정 스케줄 목록"),
+ fieldWithPath("schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("schedules[].times").type(JsonFieldType.ARRAY).description("시간 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("특정 고정 스케줄을 삭제한다.")
+ public void removeFixedEvent() throws Exception {
+ // given
+ Long fixedEventId = 1L;
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.delete("/api/v1/fixed-schedules/{id}", fixedEventId)
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("고정 스케줄 삭제에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/delete",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("특정 고정 스케줄을 삭제한다.")
+ .pathParameters(
+ parameterWithName("id").description("고정 스케줄 ID [예시 : 1 (NUMBER Type)]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("요일별 고정 스케줄을 조회한다.")
+ public void getFixedEventByDay() throws Exception {
+ // given
+ String day = "mon";
+ List responses = List.of(new FixedEventByDayResponse(1L, "고정 이벤트", "09:00", "10:00"));
+ Mockito.when(fixedEventService.getFixedEventByDay(authorizationHeader, day)).thenReturn(responses);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/fixed-schedules/by-day/{day}", day)
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("요일 별 고정 스케줄 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("fixed/getByDay",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Fixed API")
+ .description("요일별 고정 스케줄을 조회한다.")
+ .pathParameters(
+ parameterWithName("day").description("조회할 요일 [예시 : mon, tue, ...]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].id").type(JsonFieldType.NUMBER).description("고정 스케줄 ID"),
+ fieldWithPath("payload[].title").type(JsonFieldType.STRING).description("고정 스케줄 이름"),
+ fieldWithPath("payload[].start_time").type(JsonFieldType.STRING).description("시작 시간"),
+ fieldWithPath("payload[].end_time").type(JsonFieldType.STRING).description("종료 시간")
+ )
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/member/MemberControllerTest.java b/src/test/java/side/onetime/member/MemberControllerTest.java
new file mode 100644
index 0000000..307bcfd
--- /dev/null
+++ b/src/test/java/side/onetime/member/MemberControllerTest.java
@@ -0,0 +1,201 @@
+package side.onetime.member;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.MemberController;
+import side.onetime.domain.enums.Category;
+import side.onetime.dto.member.request.IsDuplicateRequest;
+import side.onetime.dto.member.request.LoginMemberRequest;
+import side.onetime.dto.member.request.RegisterMemberRequest;
+import side.onetime.dto.member.response.IsDuplicateResponse;
+import side.onetime.dto.member.response.LoginMemberResponse;
+import side.onetime.dto.member.response.RegisterMemberResponse;
+import side.onetime.dto.member.response.ScheduleResponse;
+import side.onetime.service.MemberService;
+import side.onetime.util.JwtUtil;
+
+import java.util.List;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(MemberController.class)
+public class MemberControllerTest extends ControllerTestConfig {
+
+ @MockBean
+ private MemberService memberService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Test
+ @DisplayName("멤버를 등록한다.")
+ public void registerMember() throws Exception {
+ // given
+ RegisterMemberRequest request = new RegisterMemberRequest(
+ "123e4567-e89b-12d3-a456-426614174000",
+ "newMember",
+ "1234",
+ List.of(
+ new ScheduleResponse("2024.12.01", List.of("09:00", "10:00")),
+ new ScheduleResponse("2024.12.02", List.of("11:00", "12:00"))
+ )
+ );
+ RegisterMemberResponse response = new RegisterMemberResponse("789e0123-e45b-67c8-d901-234567890abc", "CATEGORY");
+
+ Mockito.when(memberService.registerMember(any(RegisterMemberRequest.class))).thenReturn(response);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/members/action-register")
+ .content(objectMapper.writeValueAsString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("멤버 등록에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("member/register",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Member API")
+ .description("멤버를 등록한다.")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("pin").type(JsonFieldType.STRING).description("멤버 PIN"),
+ fieldWithPath("schedules").type(JsonFieldType.ARRAY).description("스케줄 목록"),
+ fieldWithPath("schedules[].time_point").type(JsonFieldType.STRING).description("스케줄 날짜"),
+ fieldWithPath("schedules[].times").type(JsonFieldType.ARRAY).description("시간 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.member_id").type(JsonFieldType.STRING).description("멤버 ID"),
+ fieldWithPath("payload.category").type(JsonFieldType.STRING).description("이벤트 카테고리")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("멤버 로그인을 진행한다.")
+ public void loginMember() throws Exception {
+ // given
+ LoginMemberRequest request = new LoginMemberRequest(
+ "123e4567-e89b-12d3-a456-426614174000",
+ "existingMember",
+ "1234"
+ );
+ LoginMemberResponse response = new LoginMemberResponse("789e0123-e45b-67c8-d901-234567890abc", "DATE");
+
+ Mockito.when(memberService.loginMember(any(LoginMemberRequest.class))).thenReturn(response);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/members/action-login")
+ .content(objectMapper.writeValueAsString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("멤버 로그인에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("member/login",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Member API")
+ .description("멤버 로그인을 진행한다.")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("pin").type(JsonFieldType.STRING).description("멤버 PIN")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.member_id").type(JsonFieldType.STRING).description("멤버 ID"),
+ fieldWithPath("payload.category").type(JsonFieldType.STRING).description("이벤트 카테고리")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("멤버 이름 중복 확인을 진행한다.")
+ public void isDuplicate() throws Exception {
+ // given
+ IsDuplicateRequest request = new IsDuplicateRequest(
+ "123e4567-e89b-12d3-a456-426614174000",
+ "duplicateCheckName"
+ );
+ IsDuplicateResponse response = new IsDuplicateResponse(true);
+
+ Mockito.when(memberService.isDuplicate(any(IsDuplicateRequest.class))).thenReturn(response);
+
+ // when
+ ResultActions result = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/members/name/action-check")
+ .content(objectMapper.writeValueAsString(request))
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ result.andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("멤버 이름 중복 확인에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("member/check-duplicate",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Member API")
+ .description("멤버 이름 중복 확인을 진행한다.")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("name").type(JsonFieldType.STRING).description("확인할 멤버 이름")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("HTTP 상태 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.is_possible").type(JsonFieldType.BOOLEAN).description("이름 사용 가능 여부")
+ )
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/schedule/ScheduleControllerTest.java b/src/test/java/side/onetime/schedule/ScheduleControllerTest.java
new file mode 100644
index 0000000..465628d
--- /dev/null
+++ b/src/test/java/side/onetime/schedule/ScheduleControllerTest.java
@@ -0,0 +1,542 @@
+package side.onetime.schedule;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.ScheduleController;
+import side.onetime.dto.schedule.request.CreateDateScheduleRequest;
+import side.onetime.dto.schedule.request.CreateDayScheduleRequest;
+import side.onetime.dto.schedule.request.GetFilteredSchedulesRequest;
+import side.onetime.dto.schedule.response.DateSchedule;
+import side.onetime.dto.schedule.response.DaySchedule;
+import side.onetime.dto.schedule.response.PerDateSchedulesResponse;
+import side.onetime.dto.schedule.response.PerDaySchedulesResponse;
+import side.onetime.service.ScheduleService;
+import side.onetime.util.JwtUtil;
+
+import java.util.List;
+import java.util.UUID;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(ScheduleController.class)
+public class ScheduleControllerTest extends ControllerTestConfig {
+ @MockBean
+ private ScheduleService scheduleService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ @Test
+ @DisplayName("요일 스케줄을 등록한다. (토큰 유무에 따라 로그인/비로그인 구분)")
+ public void createDaySchedulesForAnonymousUser() throws Exception {
+ // given
+ String eventId = "123e4567-e89b-12d3-a456-426614174000";
+ String memberId = "789e0123-e45b-67c8-d901-234567890abc";
+ List daySchedules = List.of(
+ new DaySchedule("월", List.of("09:00", "10:00"))
+ );
+ CreateDayScheduleRequest request = new CreateDayScheduleRequest(eventId, memberId, daySchedules);
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ Mockito.doNothing().when(scheduleService).createDaySchedulesForAnonymousUser(any(CreateDayScheduleRequest.class));
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/schedules/day")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("요일 스케줄 등록에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/create-day-anonymous",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("요일 스케줄을 등록한다. (비로그인의 경우에는 멤버 ID가 필수 값)")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("member_id").type(JsonFieldType.STRING).description("멤버 ID"),
+ fieldWithPath("schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("날짜 스케줄을 등록한다. (토큰 유무에 따라 로그인/비로그인 구분)")
+ public void createDateSchedulesForAuthenticatedUser() throws Exception {
+ // given
+ String eventId = "123e4567-e89b-12d3-a456-426614174000";
+ String memberId = "789e0123-e45b-67c8-d901-234567890abc";
+ List dateSchedules = List.of(
+ new DateSchedule("2024.12.01", List.of("09:00", "10:00"))
+ );
+ CreateDateScheduleRequest request = new CreateDateScheduleRequest(eventId, memberId, dateSchedules);
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ Mockito.doNothing().when(scheduleService).createDateSchedulesForAnonymousUser(any(CreateDateScheduleRequest.class));
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/schedules/date")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("날짜 스케줄 등록에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/create-date-authenticated",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("날짜 스케줄을 등록한다. (비로그인의 경우에는 멤버 ID가 필수 값)")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("member_id").type(JsonFieldType.STRING).optional().description("멤버 ID (로그인 유저는 필요 없음)"),
+ fieldWithPath("schedules[].time_point").type(JsonFieldType.STRING).description("날짜"),
+ fieldWithPath("schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("이벤트에 대한 모든 요일 스케줄을 조회한다.")
+ public void getAllDaySchedules() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ List daySchedules = List.of(new DaySchedule("월", List.of("09:00", "10:00")));
+ List responses = List.of(PerDaySchedulesResponse.of("Test Member", daySchedules));
+
+ Mockito.when(scheduleService.getAllDaySchedules(anyString())).thenReturn(responses);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/day/{event_id}", eventId)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("전체 요일 스케줄 조회에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-all-day-schedules",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("이벤트에 대한 모든 요일 스케줄을 조회한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload[].schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload[].schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("개인 요일 스케줄을 조회한다. (비로그인 유저)")
+ public void getMemberDaySchedules() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ String memberId = UUID.randomUUID().toString();
+ List daySchedules = List.of(new DaySchedule("화", List.of("11:00", "12:00")));
+ PerDaySchedulesResponse response = PerDaySchedulesResponse.of("Test Member", daySchedules);
+
+ Mockito.when(scheduleService.getMemberDaySchedules(anyString(), anyString())).thenReturn(response);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/day/{event_id}/{member_id}", eventId, memberId)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("개인(비로그인) 요일 스케줄 조회에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-member-day-schedules",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("개인 요일 스케줄을 조회한다. (비로그인 유저)")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]"),
+ parameterWithName("member_id").description("멤버 ID [예시 : 789e0123-e45b-67c8-d901-234567890abc]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload.schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload.schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("개인 요일 스케줄을 조회한다. (로그인 유저)")
+ public void getUserDaySchedules() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ String authorizationHeader = "Bearer sampleAuthToken";
+ List daySchedules = List.of(new DaySchedule("수", List.of("13:00", "14:00")));
+ PerDaySchedulesResponse response = PerDaySchedulesResponse.of("Test User", daySchedules);
+
+ Mockito.when(scheduleService.getUserDaySchedules(anyString(), anyString())).thenReturn(response);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/day/{event_id}/user", eventId)
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("개인(로그인) 요일 스케줄 조회에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-user-day-schedules",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("개인 요일 스케줄을 조회한다. (로그인 유저)")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.name").type(JsonFieldType.STRING).description("사용자 이름"),
+ fieldWithPath("payload.schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload.schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("멤버 필터링 요일 스케줄을 조회한다.")
+ public void getFilteredDaySchedules() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ List names = List.of("Test Member");
+ GetFilteredSchedulesRequest request = new GetFilteredSchedulesRequest(eventId, names);
+ List daySchedules = List.of(new DaySchedule("월", List.of("09:00", "10:00")));
+ List responses = List.of(PerDaySchedulesResponse.of("Test Member", daySchedules));
+
+ Mockito.when(scheduleService.getFilteredDaySchedules(any(GetFilteredSchedulesRequest.class))).thenReturn(responses);
+
+ // when
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/day/action-filtering")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("멤버 필터링 요일 스케줄 조회에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-filtered-day-schedules",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("멤버 필터링 요일 스케줄을 조회한다.")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("names[]").type(JsonFieldType.ARRAY).description("조회할 멤버 이름 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload[].schedules[].time_point").type(JsonFieldType.STRING).description("요일"),
+ fieldWithPath("payload[].schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("이벤트에 대한 모든 날짜 스케줄을 조회한다.")
+ public void getAllDateSchedules() throws Exception {
+ // given
+ String eventId = UUID.randomUUID().toString();
+ List dateSchedules = List.of(new DateSchedule("2024-12-01", List.of("09:00", "10:00")));
+ List responses = List.of(PerDateSchedulesResponse.of("Test Member", dateSchedules));
+
+ Mockito.when(scheduleService.getAllDateSchedules(any(String.class))).thenReturn(responses);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/date/{event_id}", eventId)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("전체 날짜 스케줄 조회에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-all-date-schedules",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("이벤트에 대한 모든 날짜 스케줄을 조회한다.")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload[].schedules[].time_point").type(JsonFieldType.STRING).description("날짜"),
+ fieldWithPath("payload[].schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("개인 날짜 스케줄을 조회한다. (비로그인 유저)")
+ public void getMemberDateSchedules() throws Exception {
+ // given
+ String eventId = "123e4567-e89b-12d3-a456-426614174000";
+ String memberId = "789e0123-e45b-67c8-d901-234567890abc";
+ PerDateSchedulesResponse response = PerDateSchedulesResponse.of("memberName", List.of(new DateSchedule("2024.12.01", List.of("09:00", "10:00"))));
+
+ Mockito.when(scheduleService.getMemberDateSchedules(eventId, memberId)).thenReturn(response);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/date/{event_id}/{member_id}", eventId, memberId)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("개인(비로그인) 날짜 스케줄 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-member-date",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("개인 날짜 스케줄을 조회한다. (비로그인 유저)")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]"),
+ parameterWithName("member_id").description("멤버 ID [예시 : 789e0123-e45b-67c8-d901-234567890abc]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload.schedules[].time_point").type(JsonFieldType.STRING).description("날짜"),
+ fieldWithPath("payload.schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("개인 날짜 스케줄을 조회한다. (로그인 유저)")
+ public void getUserDateSchedules() throws Exception {
+ // given
+ String eventId = "123e4567-e89b-12d3-a456-426614174000";
+ String authorizationHeader = "Bearer some_token";
+ PerDateSchedulesResponse response = PerDateSchedulesResponse.of("userNickname", List.of(new DateSchedule("2024.12.01", List.of("09:00", "10:00"))));
+
+ Mockito.when(scheduleService.getUserDateSchedules(eventId, authorizationHeader)).thenReturn(response);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/date/{event_id}/user", eventId)
+ .header("Authorization", authorizationHeader)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("개인(로그인) 날짜 스케줄 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-user-date",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("개인 날짜 스케줄을 조회한다. (로그인 유저)")
+ .pathParameters(
+ parameterWithName("event_id").description("이벤트 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.name").type(JsonFieldType.STRING).description("유저 닉네임"),
+ fieldWithPath("payload.schedules[].time_point").type(JsonFieldType.STRING).description("날짜"),
+ fieldWithPath("payload.schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("멤버 필터링 날짜 스케줄을 조회한다.")
+ public void getFilteredDateSchedules() throws Exception {
+ // given
+ String eventId = "123e4567-e89b-12d3-a456-426614174000";
+ List names = List.of("memberName1", "memberName2");
+ GetFilteredSchedulesRequest request = new GetFilteredSchedulesRequest(eventId, names);
+ List responseList = List.of(
+ PerDateSchedulesResponse.of("memberName1", List.of(new DateSchedule("2024.12.01", List.of("09:00", "10:00")))),
+ PerDateSchedulesResponse.of("memberName2", List.of(new DateSchedule("2024.12.02", List.of("11:00", "12:00"))))
+ );
+
+ Mockito.when(scheduleService.getFilteredDateSchedules(any(GetFilteredSchedulesRequest.class))).thenReturn(responseList);
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.get("/api/v1/schedules/date/action-filtering")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("멤버 필터링 날짜 스케줄 조회에 성공했습니다."))
+ .andDo(MockMvcRestDocumentationWrapper.document("schedule/get-filtered-date",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Schedule API")
+ .description("멤버 필터링 날짜 스케줄을 조회한다.")
+ .requestFields(
+ fieldWithPath("event_id").type(JsonFieldType.STRING).description("이벤트 ID"),
+ fieldWithPath("names[]").type(JsonFieldType.ARRAY).description("이름 목록")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload[].name").type(JsonFieldType.STRING).description("멤버 이름"),
+ fieldWithPath("payload[].schedules[].time_point").type(JsonFieldType.STRING).description("날짜"),
+ fieldWithPath("payload[].schedules[].times[]").type(JsonFieldType.ARRAY).description("스케줄 시간 목록")
+ )
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/token/TokenControllerTest.java b/src/test/java/side/onetime/token/TokenControllerTest.java
new file mode 100644
index 0000000..a16fe04
--- /dev/null
+++ b/src/test/java/side/onetime/token/TokenControllerTest.java
@@ -0,0 +1,91 @@
+package side.onetime.token;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.TokenController;
+import side.onetime.dto.token.request.ReissueTokenRequest;
+import side.onetime.dto.token.response.ReissueTokenResponse;
+import side.onetime.service.TokenService;
+import side.onetime.util.JwtUtil;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(TokenController.class)
+public class TokenControllerTest extends ControllerTestConfig {
+ @MockBean
+ private TokenService tokenService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ @Test
+ @DisplayName("액세스 토큰을 재발행한다.")
+ public void reissueTokenSuccess() throws Exception {
+ // given
+ String oldRefreshToken = "sampleOldRefreshToken";
+ String newAccessToken = "newAccessToken";
+ String newRefreshToken = "newRefreshToken";
+ ReissueTokenResponse response = ReissueTokenResponse.of(newAccessToken, newRefreshToken);
+
+ Mockito.when(tokenService.reissueToken(any(ReissueTokenRequest.class)))
+ .thenReturn(response);
+
+ ReissueTokenRequest request = new ReissueTokenRequest(oldRefreshToken);
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = mockMvc.perform(
+ RestDocumentationRequestBuilders.post("/api/v1/tokens/action-reissue")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON)
+ );
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("토큰 재발행에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.access_token").value(newAccessToken))
+ .andExpect(jsonPath("$.payload.refresh_token").value(newRefreshToken))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("token/reissue",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("Token API")
+ .description("액세스 토큰을 재발행한다.")
+ .requestFields(
+ fieldWithPath("refresh_token").type(JsonFieldType.STRING).description("기존 리프레쉬 토큰")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload.access_token").type(JsonFieldType.STRING).description("새로운 액세스 토큰"),
+ fieldWithPath("payload.refresh_token").type(JsonFieldType.STRING).description("새로운 리프레쉬 토큰")
+ )
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/url/UrlControllerTest.java b/src/test/java/side/onetime/url/UrlControllerTest.java
new file mode 100644
index 0000000..2ef690c
--- /dev/null
+++ b/src/test/java/side/onetime/url/UrlControllerTest.java
@@ -0,0 +1,146 @@
+package side.onetime.url;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.UrlController;
+import side.onetime.dto.url.request.ConvertToOriginalUrlRequest;
+import side.onetime.dto.url.request.ConvertToShortenUrlRequest;
+import side.onetime.dto.url.response.ConvertToOriginalUrlResponse;
+import side.onetime.dto.url.response.ConvertToShortenUrlResponse;
+import side.onetime.repository.EventRepository;
+import side.onetime.service.UrlService;
+import side.onetime.util.JwtUtil;
+
+import java.util.UUID;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(UrlController.class)
+public class UrlControllerTest extends ControllerTestConfig {
+ @MockBean
+ private UrlService urlService;
+
+ @MockBean
+ private EventRepository eventRepository;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ @Test
+ @DisplayName("원본 URL을 단축 URL로 변환한다.")
+ public void convertToShortenUrl() throws Exception {
+ // given
+ String originalUrl = "https://example.com/event/123e4567-e89b-12d3-a456-426614174000";
+ ConvertToShortenUrlRequest request = new ConvertToShortenUrlRequest(originalUrl);
+ String shortenUrl = "https://short.ly/abc123";
+
+ Mockito.when(eventRepository.existsByEventId(any(UUID.class))).thenReturn(true);
+ Mockito.when(urlService.convertToShortenUrl(any(ConvertToShortenUrlRequest.class)))
+ .thenReturn(ConvertToShortenUrlResponse.of(shortenUrl));
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/urls/action-shorten")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestContent)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("단축 URL 변환에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.shorten_url").value(shortenUrl))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("url/convert-to-shorten",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("URL API")
+ .description("원본 URL을 단축 URL로 변환한다.")
+ .requestFields(
+ fieldWithPath("original_url").type(JsonFieldType.STRING).description("단축할 원본 URL")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.shorten_url").type(JsonFieldType.STRING).description("생성된 단축 URL")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("단축 URL을 원본 URL로 변환한다.")
+ public void convertToOriginalUrl() throws Exception {
+ // given
+ String shortenUrl = "https://short.ly/abc123";
+ ConvertToOriginalUrlRequest request = new ConvertToOriginalUrlRequest(shortenUrl);
+ String originalUrl = "https://example.com/event/123e4567-e89b-12d3-a456-426614174000";
+
+ Mockito.when(urlService.convertToOriginalUrl(any(ConvertToOriginalUrlRequest.class)))
+ .thenReturn(ConvertToOriginalUrlResponse.of(originalUrl));
+ Mockito.when(eventRepository.existsByEventId(any(UUID.class))).thenReturn(true);
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/urls/action-original")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestContent)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("원본 URL 변환에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.original_url").value(originalUrl))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("url/convert-to-original",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("URL API")
+ .description("단축 URL을 원본 URL로 변환한다.")
+ .requestFields(
+ fieldWithPath("shorten_url").type(JsonFieldType.STRING).description("복원할 단축 URL")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.original_url").type(JsonFieldType.STRING).description("복원된 원본 URL")
+ )
+ .build()
+ )
+ ));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/side/onetime/user/UserControllerTest.java b/src/test/java/side/onetime/user/UserControllerTest.java
new file mode 100644
index 0000000..1a5c30b
--- /dev/null
+++ b/src/test/java/side/onetime/user/UserControllerTest.java
@@ -0,0 +1,221 @@
+package side.onetime.user;
+
+import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
+import com.epages.restdocs.apispec.ResourceSnippetParameters;
+import com.epages.restdocs.apispec.Schema;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
+import org.springframework.restdocs.payload.JsonFieldType;
+import org.springframework.test.web.servlet.ResultActions;
+import side.onetime.configuration.ControllerTestConfig;
+import side.onetime.controller.UserController;
+import side.onetime.dto.user.request.OnboardUserRequest;
+import side.onetime.dto.user.request.UpdateUserProfileRequest;
+import side.onetime.dto.user.response.GetUserProfileResponse;
+import side.onetime.dto.user.response.OnboardUserResponse;
+import side.onetime.service.UserService;
+import side.onetime.util.JwtUtil;
+
+import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(UserController.class)
+public class UserControllerTest extends ControllerTestConfig {
+ @MockBean
+ private UserService userService;
+
+ @MockBean
+ private JwtUtil jwtUtil;
+
+ @Test
+ @DisplayName("유저 온보딩을 진행한다.")
+ public void onboardUser() throws Exception {
+ // given
+ OnboardUserResponse response = new OnboardUserResponse("sampleAccessToken", "sampleRefreshToken");
+ Mockito.when(userService.onboardUser(any(OnboardUserRequest.class))).thenReturn(response);
+
+ OnboardUserRequest request = new OnboardUserRequest("sampleRegisterToken", "UserNickname");
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/onboarding")
+ .content(requestContent)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("201"))
+ .andExpect(jsonPath("$.message").value("유저 온보딩에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.access_token").value("sampleAccessToken"))
+ .andExpect(jsonPath("$.payload.refresh_token").value("sampleRefreshToken"))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("user/onboard",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("User API")
+ .description("유저 온보딩을 진행한다.")
+ .requestFields(
+ fieldWithPath("register_token").type(JsonFieldType.STRING).description("레지스터 토큰"),
+ fieldWithPath("nickname").type(JsonFieldType.STRING).description("유저 닉네임")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("응답 데이터"),
+ fieldWithPath("payload.access_token").type(JsonFieldType.STRING).description("액세스 토큰"),
+ fieldWithPath("payload.refresh_token").type(JsonFieldType.STRING).description("리프레쉬 토큰")
+ )
+ .requestSchema(Schema.schema("OnboardUserRequestSchema"))
+ .responseSchema(Schema.schema("OnboardUserResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저 정보를 조회한다.")
+ public void getUserProfile() throws Exception {
+ // given
+ String nickname = "UserNickname";
+ String email = "user@example.com";
+ GetUserProfileResponse response = new GetUserProfileResponse(nickname, email);
+
+ Mockito.when(userService.getUserProfile(anyString())).thenReturn(response);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/users/profile")
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저 정보 조회에 성공했습니다."))
+ .andExpect(jsonPath("$.payload.nickname").value(nickname))
+ .andExpect(jsonPath("$.payload.email").value(email))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("user/get-profile",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("User API")
+ .description("유저 정보를 조회한다.")
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
+ fieldWithPath("payload").type(JsonFieldType.OBJECT).description("유저 정보 데이터"),
+ fieldWithPath("payload.nickname").type(JsonFieldType.STRING).description("유저 닉네임"),
+ fieldWithPath("payload.email").type(JsonFieldType.STRING).description("유저 이메일")
+ )
+ .responseSchema(Schema.schema("GetUserProfileResponseSchema"))
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저 정보를 수정한다.")
+ public void updateUserProfile() throws Exception {
+ // given
+ UpdateUserProfileRequest request = new UpdateUserProfileRequest("NewNickname");
+
+ Mockito.doNothing().when(userService).updateUserProfile(anyString(), any(UpdateUserProfileRequest.class));
+
+ String requestContent = new ObjectMapper().writeValueAsString(request);
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/users/profile/action-update")
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(requestContent)
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저 정보 수정에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("user/update-profile",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("User API")
+ .description("유저 정보를 수정한다.")
+ .requestFields(
+ fieldWithPath("nickname").type(JsonFieldType.STRING).description("수정할 닉네임")
+ )
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+
+ @Test
+ @DisplayName("유저가 서비스를 탈퇴한다.")
+ public void withdrawService() throws Exception {
+ // given
+ Mockito.doNothing().when(userService).withdrawService(anyString());
+
+ // when
+ ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/users/action-withdraw")
+ .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken")
+ .accept(MediaType.APPLICATION_JSON));
+
+ // then
+ resultActions
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.is_success").value(true))
+ .andExpect(jsonPath("$.code").value("200"))
+ .andExpect(jsonPath("$.message").value("유저 서비스 탈퇴에 성공했습니다."))
+
+ // docs
+ .andDo(MockMvcRestDocumentationWrapper.document("user/withdraw-service",
+ preprocessRequest(prettyPrint()),
+ preprocessResponse(prettyPrint()),
+ resource(
+ ResourceSnippetParameters.builder()
+ .tag("User API")
+ .description("유저가 서비스를 탈퇴한다.")
+ .responseFields(
+ fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),
+ fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"),
+ fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지")
+ )
+ .build()
+ )
+ ));
+ }
+}