diff --git a/src/main/java/redot/redot_server/RedotServerApplication.java b/src/main/java/redot/redot_server/RedotServerApplication.java index 3afdf73..c621fbd 100644 --- a/src/main/java/redot/redot_server/RedotServerApplication.java +++ b/src/main/java/redot/redot_server/RedotServerApplication.java @@ -3,12 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import redot.redot_server.domain.redot.consultation.config.ConsultationNotificationProperties; import redot.redot_server.global.email.EmailVerificationProperties; import redot.redot_server.global.s3.config.ImageUploadProperties; import redot.redot_server.global.security.social.config.AuthRedirectProperties; @SpringBootApplication -@EnableConfigurationProperties({AuthRedirectProperties.class, EmailVerificationProperties.class, ImageUploadProperties.class}) +@EnableConfigurationProperties({AuthRedirectProperties.class, EmailVerificationProperties.class, ImageUploadProperties.class, ConsultationNotificationProperties.class}) public class RedotServerApplication { public static void main(String[] args) { SpringApplication.run(RedotServerApplication.class, args); diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java index f7ca78a..8b2f11d 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.admin.controller.docs.AdminConsultationControllerDocs; import redot.redot_server.domain.admin.dto.ConsultationSearchCondition; import redot.redot_server.domain.admin.dto.request.ConsultationUpdateRequest; import redot.redot_server.domain.admin.service.AdminConsultationService; @@ -21,17 +22,19 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/redot/admin/consultations") -public class AdminConsultationController { +public class AdminConsultationController implements AdminConsultationControllerDocs { private final AdminConsultationService adminConsultationService; @GetMapping("/{consultationId}") + @Override public ResponseEntity getConsultationInfo( @PathVariable("consultationId") Long consultationId) { return ResponseEntity.ok(adminConsultationService.getConsultationInfo(consultationId)); } @GetMapping + @Override public ResponseEntity> getAllConsultations( ConsultationSearchCondition searchCondition, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { @@ -41,6 +44,7 @@ public ResponseEntity> getAllConsultations( } @PutMapping("/{consultationId}") + @Override public ResponseEntity updateConsultationInfo( @RequestBody @Valid ConsultationUpdateRequest request, @PathVariable("consultationId") Long consultationId) { diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java index 10181fe..3749351 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminController.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.admin.controller.docs.AdminControllerDocs; import redot.redot_server.domain.admin.dto.request.AdminCreateRequest; import redot.redot_server.domain.admin.dto.request.AdminResetPasswordRequest; import redot.redot_server.domain.admin.dto.response.AdminResponse; @@ -33,38 +34,44 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/redot/admin") -public class AdminController { +public class AdminController implements AdminControllerDocs { private final AdminService adminService; private final TokenCookieFactory tokenCookieFactory; @GetMapping("/{adminId}") + @Override public ResponseEntity getAdminInfo(@PathVariable("adminId") Long adminId) { return ResponseEntity.ok(adminService.getAdminInfo(adminId)); } @PostMapping + @Override public ResponseEntity createAdmin(@Valid @RequestBody AdminCreateRequest request) { return ResponseEntity.ok(adminService.createAdmin(request)); } @GetMapping + @Override public ResponseEntity> getAdminInfoList(Pageable pageable) { return ResponseEntity.ok(adminService.getAdminInfoList(pageable)); } @PutMapping("/{adminId}") + @Override public ResponseEntity updateAdmin(@PathVariable("adminId") Long adminId, @Valid @RequestBody AdminUpdateRequest request) { return ResponseEntity.ok(adminService.updateAdmin(adminId, request)); } @PostMapping("/{adminId}/reset-password") + @Override public ResponseEntity resetAdminPassword(@PathVariable("adminId") Long adminId, @Valid @RequestBody AdminResetPasswordRequest request) { adminService.resetAdminPassword(adminId, request); return ResponseEntity.noContent().build(); } @DeleteMapping("/{adminId}") + @Override public ResponseEntity deleteAdmin(@PathVariable("adminId") Long adminId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { adminService.deleteAdmin(jwtPrincipal.id(), adminId); @@ -72,6 +79,7 @@ public ResponseEntity deleteAdmin(@PathVariable("adminId") Long adminId, @ } @DeleteMapping + @Override public ResponseEntity deleteCurrentAdmin(HttpServletRequest request, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { adminService.deleteCurrentAdmin(jwtPrincipal.id()); ResponseCookie deleteAccess = tokenCookieFactory.deleteAccessTokenCookie(request, TokenType.ADMIN.getAccessCookieName()); @@ -84,6 +92,7 @@ public ResponseEntity deleteCurrentAdmin(HttpServletRequest request, @Auth } @PostMapping("/upload-profile-image") + @Override public ResponseEntity uploadProfileImage( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @RequestPart("image") @NotNull MultipartFile image diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java index 233b263..42dca48 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminRedotAppController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.admin.controller.docs.AdminRedotAppControllerDocs; import redot.redot_server.domain.admin.service.AdminRedotAppService; import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest; import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse; @@ -14,11 +15,12 @@ @RestController @RequestMapping("/api/v1/redot/admin/app") @RequiredArgsConstructor -public class AdminRedotAppController { +public class AdminRedotAppController implements AdminRedotAppControllerDocs { private final AdminRedotAppService redotAppService; @PostMapping + @Override public ResponseEntity createRedotApp(@Valid @RequestBody RedotAppCreateRequest request) { return ResponseEntity.ok(redotAppService.createRedotApp(request)); } diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java new file mode 100644 index 0000000..97a397d --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java @@ -0,0 +1,37 @@ +package redot.redot_server.domain.admin.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.admin.dto.ConsultationSearchCondition; +import redot.redot_server.domain.admin.dto.request.ConsultationUpdateRequest; +import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "Admin Consultation", description = "관리자 상담 관리 API") +public interface AdminConsultationControllerDocs { + + @Operation(summary = "상담 단건 조회", description = "상담 ID로 상담 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) + ResponseEntity getConsultationInfo(@Parameter(description = "상담 ID", example = "1") Long consultationId); + + @Operation(summary = "상담 목록 조회", description = "`email`, `phone`, `status`, `type`, `currentWebsiteUrl` 검색 조건을 조합해 조회하며 `sort=createdAt,desc` 와 같은 Pageable 쿼리 파라미터를 사용합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getAllConsultations( + @ParameterObject ConsultationSearchCondition searchCondition, + @Parameter(description = "기본 정렬은 createdAt DESC 입니다.") @ParameterObject Pageable pageable); + + @Operation(summary = "상담 정보 수정", description = "상담 상태나 담당자 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) + ResponseEntity updateConsultationInfo(ConsultationUpdateRequest request, + @Parameter(description = "상담 ID", example = "1") Long consultationId); +} diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java new file mode 100644 index 0000000..bedda64 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminControllerDocs.java @@ -0,0 +1,67 @@ +package redot.redot_server.domain.admin.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.admin.dto.request.AdminCreateRequest; +import redot.redot_server.domain.admin.dto.request.AdminResetPasswordRequest; +import redot.redot_server.domain.admin.dto.request.AdminUpdateRequest; +import redot.redot_server.domain.admin.dto.response.AdminResponse; +import redot.redot_server.global.s3.dto.UploadedImageUrlResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "Admin", description = "Redot 관리자 API") +public interface AdminControllerDocs { + + @Operation(summary = "관리자 단건 조회", description = "지정한 ID의 관리자 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = AdminResponse.class))) + ResponseEntity getAdminInfo(@Parameter(description = "관리자 ID", example = "1") Long adminId); + + @Operation(summary = "관리자 생성", description = "새로운 관리자를 생성합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = AdminResponse.class))) + ResponseEntity createAdmin(AdminCreateRequest request); + + @Operation(summary = "관리자 목록 조회", description = "`page`, `size`, `sort` 파라미터를 사용해 페이징/정렬하며 `sort=필드명,정렬방향` (예: `sort=createdAt,desc`) 형식을 따릅니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getAdminInfoList(@Parameter(description = "기본적으로 page=0, size=20이며 sort 파라미터는 `createdAt,desc`와 같이 전달합니다.") + @ParameterObject Pageable pageable); + + @Operation(summary = "관리자 정보 수정", description = "지정된 관리자의 프로필 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = AdminResponse.class))) + ResponseEntity updateAdmin(@Parameter(description = "관리자 ID", example = "1") Long adminId, + AdminUpdateRequest request); + + @Operation(summary = "관리자 비밀번호 초기화", description = "관리자의 비밀번호를 재설정합니다.") + @ApiResponse(responseCode = "204", description = "재설정 완료") + ResponseEntity resetAdminPassword(@Parameter(description = "관리자 ID", example = "1") Long adminId, + AdminResetPasswordRequest request); + + @Operation(summary = "다른 관리자 삭제", description = "현재 로그인한 관리자가 다른 관리자를 삭제합니다.") + @ApiResponse(responseCode = "204", description = "삭제 완료") + ResponseEntity deleteAdmin(@Parameter(description = "삭제 대상 관리자 ID", example = "2") Long adminId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "현재 관리자 탈퇴", description = "본인의 계정을 삭제하고 토큰 쿠키를 제거합니다.") + @ApiResponse(responseCode = "204", description = "탈퇴 완료") + ResponseEntity deleteCurrentAdmin(@Parameter(hidden = true) HttpServletRequest request, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "관리자 프로필 이미지 업로드", description = "관리자 프로필 이미지를 업로드하고 경로를 반환합니다.") + @ApiResponse(responseCode = "200", description = "업로드 성공", + content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) + ResponseEntity uploadProfileImage(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, + MultipartFile image); +} diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java new file mode 100644 index 0000000..8956d33 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminRedotAppControllerDocs.java @@ -0,0 +1,19 @@ +package redot.redot_server.domain.admin.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest; +import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse; + +@Tag(name = "Admin Redot App", description = "관리자 앱 관리 API") +public interface AdminRedotAppControllerDocs { + + @Operation(summary = "Redot 앱 생성", description = "관리자 권한으로 신규 Redot 앱을 생성합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class))) + ResponseEntity createRedotApp(RedotAppCreateRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/auth/controller/AdminAuthController.java b/src/main/java/redot/redot_server/domain/auth/controller/AdminAuthController.java index 0997234..2a59daf 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/AdminAuthController.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/AdminAuthController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.auth.controller.docs.AdminAuthControllerDocs; import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; import redot.redot_server.domain.auth.dto.request.SignInRequest; import redot.redot_server.domain.auth.dto.response.AuthResult; @@ -27,13 +28,14 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/auth/redot/admin") -public class AdminAuthController { +public class AdminAuthController implements AdminAuthControllerDocs { private final AdminAuthService adminAuthService; private final AdminService adminService; private final TokenCookieFactory tokenCookieFactory; @PostMapping("/sign-in") + @Override public ResponseEntity signIn(HttpServletRequest request, @Valid @RequestBody SignInRequest signInRequest) { AuthResult authResult = adminAuthService.signIn(request, signInRequest); return ResponseEntity.ok() @@ -43,11 +45,13 @@ public ResponseEntity signIn(HttpServletRequest request, @Valid @ } @PostMapping("/sign-up") + @Override public ResponseEntity createAdmin(@Valid @RequestBody AdminCreateRequest request) { return ResponseEntity.ok(adminService.createAdmin(request)); } @PostMapping("/reissue" ) + @Override public ResponseEntity refreshToken(HttpServletRequest request) { AuthResult authResult = adminAuthService.reissueToken(request); @@ -58,12 +62,14 @@ public ResponseEntity refreshToken(HttpServletRequest request) { } @GetMapping("/me") + @Override public ResponseEntity getCurrentAdminInfo(@AuthenticationPrincipal JwtPrincipal jwtPrincipal) { AdminResponse adminResponse = adminAuthService.getCurrentAdminInfo(jwtPrincipal.id()); return ResponseEntity.ok(adminResponse); } @PostMapping("/sign-out") + @Override public ResponseEntity signOut(HttpServletRequest request) { ResponseCookie deleteAccess = tokenCookieFactory.deleteAccessTokenCookie(request, TokenType.ADMIN.getAccessCookieName()); ResponseCookie deleteRefresh = tokenCookieFactory.deleteRefreshTokenCookie(request, TokenType.ADMIN.getRefreshCookieName()); @@ -75,6 +81,7 @@ public ResponseEntity signOut(HttpServletRequest request) { } @PostMapping("/password-reset") + @Override public ResponseEntity confirmPasswordReset(@RequestBody @Valid PasswordResetConfirmRequest request) { adminAuthService.resetPassword(request); return ResponseEntity.noContent().build(); diff --git a/src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java b/src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java index e3aedbc..addb18b 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/AdminImpersonationController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.auth.controller.docs.AdminImpersonationControllerDocs; import redot.redot_server.domain.auth.dto.response.AuthResult; import redot.redot_server.domain.auth.dto.request.CMSAdminImpersonationRequest; import redot.redot_server.domain.auth.dto.response.TokenResponse; @@ -19,10 +20,11 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/auth/redot/admin/impersonation") -public class AdminImpersonationController { +public class AdminImpersonationController implements AdminImpersonationControllerDocs { private final AdminImpersonationService adminImpersonationService; @PostMapping("/cms-admin") + @Override public ResponseEntity impersonateAsCMSAdmin(HttpServletRequest request, @Valid @RequestBody CMSAdminImpersonationRequest cmsAdminImpersonationRequest, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { Long adminId = jwtPrincipal.id(); diff --git a/src/main/java/redot/redot_server/domain/auth/controller/CMSAuthController.java b/src/main/java/redot/redot_server/domain/auth/controller/CMSAuthController.java index f2f4c29..bb363e4 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/CMSAuthController.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/CMSAuthController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.auth.controller.docs.CMSAuthControllerDocs; import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; import redot.redot_server.domain.auth.dto.request.SignInRequest; import redot.redot_server.domain.auth.dto.response.AuthResult; @@ -26,12 +27,13 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/auth/app/cms") -public class CMSAuthController { +public class CMSAuthController implements CMSAuthControllerDocs { private final CMSAuthService cmsAuthService; private final TokenCookieFactory tokenCookieFactory; @PostMapping("/sign-in") + @Override public ResponseEntity signIn(HttpServletRequest request, @Valid @RequestBody SignInRequest signInRequest, @CurrentRedotApp Long redotAppId) { AuthResult authResult = cmsAuthService.signIn(request, signInRequest, redotAppId); return ResponseEntity.ok() @@ -41,6 +43,7 @@ public ResponseEntity signIn(HttpServletRequest request, @Valid @ } @PostMapping("/reissue") + @Override public ResponseEntity reissueToken(@CurrentRedotApp Long redotAppId, HttpServletRequest request) { AuthResult authResult = cmsAuthService.reissueToken(redotAppId, request); @@ -51,12 +54,14 @@ public ResponseEntity reissueToken(@CurrentRedotApp Long redotApp } @GetMapping("/me") + @Override public ResponseEntity getCurrentCMSMemberInfo(@CurrentRedotApp Long redotAppId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { return ResponseEntity.ok(cmsAuthService.getCurrentCMSMemberInfo(redotAppId, jwtPrincipal.id())); } @PostMapping("/sign-out") + @Override public ResponseEntity signOut(HttpServletRequest request) { ResponseCookie deleteAccess = tokenCookieFactory.deleteAccessTokenCookie(request, TokenType.CMS.getAccessCookieName()); ResponseCookie deleteRefresh = tokenCookieFactory.deleteRefreshTokenCookie(request, TokenType.CMS.getRefreshCookieName()); @@ -68,6 +73,7 @@ public ResponseEntity signOut(HttpServletRequest request) { } @PostMapping("/password-reset") + @Override public ResponseEntity confirmPasswordReset(@CurrentRedotApp Long redotAppId, @RequestBody @Valid PasswordResetConfirmRequest request) { cmsAuthService.resetPassword(redotAppId, request); diff --git a/src/main/java/redot/redot_server/domain/auth/controller/EmailVerificationController.java b/src/main/java/redot/redot_server/domain/auth/controller/EmailVerificationController.java index 52b00ce..057fab2 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/EmailVerificationController.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/EmailVerificationController.java @@ -1,6 +1,5 @@ package redot.redot_server.domain.auth.controller; -import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -8,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.auth.controller.docs.EmailVerificationControllerDocs; import redot.redot_server.domain.auth.dto.request.EmailVerificationSendRequest; import redot.redot_server.domain.auth.dto.request.EmailVerificationVerifyRequest; import redot.redot_server.domain.auth.dto.response.EmailVerificationSendResponse; @@ -17,19 +17,19 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/auth/email-verification") -public class EmailVerificationController { +public class EmailVerificationController implements EmailVerificationControllerDocs { private final EmailVerificationService emailVerificationService; @PostMapping("/send") - @Operation(summary = "이메일 인증 코드 발송") + @Override public ResponseEntity send(@RequestBody @Valid EmailVerificationSendRequest request) { EmailVerificationSendResponse response = emailVerificationService.sendCode(request); return ResponseEntity.ok(response); } @PostMapping("/verify") - @Operation(summary = "이메일 인증 코드 검증") + @Override public ResponseEntity verify(@RequestBody @Valid EmailVerificationVerifyRequest request) { EmailVerificationVerifyResponse response = emailVerificationService.verifyCode(request); return ResponseEntity.ok(response); diff --git a/src/main/java/redot/redot_server/domain/auth/controller/RedotMemberAuthController.java b/src/main/java/redot/redot_server/domain/auth/controller/RedotMemberAuthController.java index 9bce24e..2acac36 100644 --- a/src/main/java/redot/redot_server/domain/auth/controller/RedotMemberAuthController.java +++ b/src/main/java/redot/redot_server/domain/auth/controller/RedotMemberAuthController.java @@ -1,6 +1,5 @@ package redot.redot_server.domain.auth.controller; -import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import java.net.URI; @@ -17,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; +import redot.redot_server.domain.auth.controller.docs.RedotMemberAuthControllerDocs; import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; import redot.redot_server.domain.auth.dto.request.RedotMemberSignInRequest; import redot.redot_server.domain.auth.dto.response.AuthResult; @@ -36,7 +36,7 @@ @RestController @RequestMapping("/api/v1/auth/redot/member") -public class RedotMemberAuthController { +public class RedotMemberAuthController implements RedotMemberAuthControllerDocs { private final RedotMemberAuthService redotMemberAuthService; private final TokenCookieFactory tokenCookieFactory; @@ -54,11 +54,13 @@ public RedotMemberAuthController(RedotMemberAuthService redotMemberAuthService, } @PostMapping("/sign-up") + @Override public ResponseEntity signUp(@RequestBody @Valid RedotMemberCreateRequest request) { return ResponseEntity.ok(redotMemberAuthService.signUp(request)); } @PostMapping("/sign-in") + @Override public ResponseEntity signIn(HttpServletRequest request, @RequestBody @Valid RedotMemberSignInRequest signInRequest) { AuthResult authResult = redotMemberAuthService.signIn(request, signInRequest); @@ -69,6 +71,7 @@ public ResponseEntity signIn(HttpServletRequest request, } @PostMapping("/reissue") + @Override public ResponseEntity reissue(HttpServletRequest request) { AuthResult authResult = redotMemberAuthService.reissue(request); return ResponseEntity.ok() @@ -78,6 +81,7 @@ public ResponseEntity reissue(HttpServletRequest request) { } @GetMapping("/me") + @Override public ResponseEntity getCurrentMember(@AuthenticationPrincipal JwtPrincipal jwtPrincipal) { if (jwtPrincipal == null || jwtPrincipal.tokenType() != TokenType.REDOT_MEMBER) { throw new AuthException(AuthErrorCode.INVALID_TOKEN_TYPE); @@ -86,6 +90,7 @@ public ResponseEntity getCurrentMember(@AuthenticationPrinc } @PostMapping("/sign-out") + @Override public ResponseEntity signOut(HttpServletRequest request) { ResponseCookie deleteAccess = tokenCookieFactory.deleteAccessTokenCookie(request, TokenType.REDOT_MEMBER.getAccessCookieName()); ResponseCookie deleteRefresh = tokenCookieFactory.deleteRefreshTokenCookie(request, TokenType.REDOT_MEMBER.getRefreshCookieName()); @@ -97,14 +102,14 @@ public ResponseEntity signOut(HttpServletRequest request) { } @PostMapping("/password-reset") + @Override public ResponseEntity confirmPasswordReset(@RequestBody @Valid PasswordResetConfirmRequest request) { redotMemberAuthService.resetPassword(request); return ResponseEntity.noContent().build(); } @GetMapping("/social/login-url") - @Operation(summary = "소셜 로그인 인가 URL 조회", - description = "프론트엔드가 브라우저를 리다이렉트하기 위해 OAuth2 인가 URL을 받아갈 때 사용합니다.") + @Override public ResponseEntity getSocialLoginUrl(@RequestParam(name = "provider", defaultValue = "google") String provider, @RequestParam(name = "redirect_uri", required = false) String redirectUri, @RequestParam(name = "failure_uri", required = false) String failureUri) { diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java new file mode 100644 index 0000000..3fcbac7 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminAuthControllerDocs.java @@ -0,0 +1,49 @@ +package redot.redot_server.domain.auth.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.admin.dto.request.AdminCreateRequest; +import redot.redot_server.domain.admin.dto.response.AdminResponse; +import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; +import redot.redot_server.domain.auth.dto.request.SignInRequest; +import redot.redot_server.domain.auth.dto.response.TokenResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; + +@Tag(name = "Admin Auth", description = "Redot 관리자 인증 API") +public interface AdminAuthControllerDocs { + + @Operation(summary = "관리자 로그인", description = "Redot 관리자 계정으로 로그인합니다.") + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, + SignInRequest signInRequest); + + @Operation(summary = "관리자 회원가입", description = "새로운 Redot 관리자를 등록합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = AdminResponse.class))) + ResponseEntity createAdmin(AdminCreateRequest request); + + @Operation(summary = "관리자 토큰 재발급", description = "만료 직전의 토큰을 쿠키 기반으로 재발급합니다.") + @ApiResponse(responseCode = "200", description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity refreshToken(@Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "현재 관리자 정보 조회", description = "로그인된 관리자 정보를 확인합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = AdminResponse.class))) + ResponseEntity getCurrentAdminInfo(@Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "관리자 로그아웃", description = "관리자 토큰 쿠키를 삭제합니다.") + @ApiResponse(responseCode = "204", description = "로그아웃 완료") + ResponseEntity signOut(@Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "관리자 비밀번호 재설정 확정", description = "비밀번호 재설정 코드를 확인하고 새 비밀번호를 저장합니다.") + @ApiResponse(responseCode = "204", description = "재설정 완료") + ResponseEntity confirmPasswordReset(PasswordResetConfirmRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java new file mode 100644 index 0000000..27bbebb --- /dev/null +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/AdminImpersonationControllerDocs.java @@ -0,0 +1,24 @@ +package redot.redot_server.domain.auth.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.auth.dto.request.CMSAdminImpersonationRequest; +import redot.redot_server.domain.auth.dto.response.TokenResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; + +@Tag(name = "Admin Impersonation", description = "관리자 권한 위임 API") +public interface AdminImpersonationControllerDocs { + + @Operation(summary = "CMS 관리자 사칭 토큰 발급", description = "최상위 관리자가 CMS 관리자 권한으로 임시 토큰을 발급받습니다.") + @ApiResponse(responseCode = "200", description = "발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity impersonateAsCMSAdmin(@Parameter(hidden = true) HttpServletRequest request, + CMSAdminImpersonationRequest cmsAdminImpersonationRequest, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); +} diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java new file mode 100644 index 0000000..8ecfa5a --- /dev/null +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/CMSAuthControllerDocs.java @@ -0,0 +1,47 @@ +package redot.redot_server.domain.auth.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; +import redot.redot_server.domain.auth.dto.request.SignInRequest; +import redot.redot_server.domain.auth.dto.response.TokenResponse; +import redot.redot_server.domain.cms.member.dto.response.CMSMemberResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; + +@Tag(name = "CMS Auth", description = "CMS 인증 API") +public interface CMSAuthControllerDocs { + + @Operation(summary = "CMS 로그인", description = "CMS 계정으로 로그인하여 액세스/리프레시 토큰을 발급합니다.") + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, + SignInRequest signInRequest, + @Parameter(hidden = true) Long redotAppId); + + @Operation(summary = "CMS 토큰 재발급", description = "요청 쿠키에 포함된 리프레시 토큰으로 새 토큰을 발급합니다.") + @ApiResponse(responseCode = "200", description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity reissueToken(@Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "현재 CMS 멤버 정보 조회", description = "인증된 CMS 멤버의 상세 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) + ResponseEntity getCurrentCMSMemberInfo(@Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "CMS 로그아웃", description = "CMS 토큰 쿠키를 삭제하여 로그아웃합니다.") + @ApiResponse(responseCode = "204", description = "로그아웃 성공") + ResponseEntity signOut(@Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "CMS 비밀번호 재설정 확정", description = "발급받은 인증 코드로 비밀번호 재설정을 확정합니다.") + @ApiResponse(responseCode = "204", description = "재설정 완료") + ResponseEntity confirmPasswordReset(@Parameter(hidden = true) Long redotAppId, + PasswordResetConfirmRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java new file mode 100644 index 0000000..24b4f06 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/EmailVerificationControllerDocs.java @@ -0,0 +1,26 @@ +package redot.redot_server.domain.auth.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.auth.dto.request.EmailVerificationSendRequest; +import redot.redot_server.domain.auth.dto.request.EmailVerificationVerifyRequest; +import redot.redot_server.domain.auth.dto.response.EmailVerificationSendResponse; +import redot.redot_server.domain.auth.dto.response.EmailVerificationVerifyResponse; + +@Tag(name = "Email Verification", description = "이메일 인증 API") +public interface EmailVerificationControllerDocs { + + @Operation(summary = "이메일 인증 코드 발송", description = "입력한 이메일로 인증 코드를 발송합니다.") + @ApiResponse(responseCode = "200", description = "발송 성공", + content = @Content(schema = @Schema(implementation = EmailVerificationSendResponse.class))) + ResponseEntity send(EmailVerificationSendRequest request); + + @Operation(summary = "이메일 인증 코드 검증", description = "발송된 인증 코드가 유효한지 검증합니다.") + @ApiResponse(responseCode = "200", description = "검증 성공", + content = @Content(schema = @Schema(implementation = EmailVerificationVerifyResponse.class))) + ResponseEntity verify(EmailVerificationVerifyRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java b/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java new file mode 100644 index 0000000..35d9fc3 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/auth/controller/docs/RedotMemberAuthControllerDocs.java @@ -0,0 +1,58 @@ +package redot.redot_server.domain.auth.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.auth.dto.request.PasswordResetConfirmRequest; +import redot.redot_server.domain.auth.dto.request.RedotMemberSignInRequest; +import redot.redot_server.domain.auth.dto.response.SocialLoginUrlResponse; +import redot.redot_server.domain.auth.dto.response.TokenResponse; +import redot.redot_server.domain.redot.member.dto.request.RedotMemberCreateRequest; +import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; + +@Tag(name = "Redot Member Auth", description = "Redot 회원 인증 API") +public interface RedotMemberAuthControllerDocs { + + @Operation(summary = "Redot 회원 회원가입", description = "새로운 Redot 회원 계정을 생성합니다.") + @ApiResponse(responseCode = "200", description = "회원가입 성공", + content = @Content(schema = @Schema(implementation = RedotMemberResponse.class))) + ResponseEntity signUp(RedotMemberCreateRequest request); + + @Operation(summary = "Redot 회원 로그인", description = "Redot 회원 자격 증명을 사용해 로그인합니다.") + @ApiResponse(responseCode = "200", description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity signIn(@Parameter(hidden = true) HttpServletRequest request, + RedotMemberSignInRequest signInRequest); + + @Operation(summary = "Redot 회원 토큰 재발급", description = "로그인된 회원의 토큰을 재발급합니다.") + @ApiResponse(responseCode = "200", description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))) + ResponseEntity reissue(@Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "현재 Redot 회원 정보 조회", description = "로그인된 회원 정보를 확인합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = RedotMemberResponse.class))) + ResponseEntity getCurrentMember(@Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "Redot 회원 로그아웃", description = "회원 토큰 쿠키를 삭제하여 로그아웃합니다.") + @ApiResponse(responseCode = "204", description = "로그아웃 완료") + ResponseEntity signOut(@Parameter(hidden = true) HttpServletRequest request); + + @Operation(summary = "Redot 회원 비밀번호 재설정 확정", description = "비밀번호 재설정 토큰을 확인하고 비밀번호를 교체합니다.") + @ApiResponse(responseCode = "204", description = "재설정 완료") + ResponseEntity confirmPasswordReset(PasswordResetConfirmRequest request); + + @Operation(summary = "소셜 로그인 인가 URL 조회", description = "선택한 소셜 제공자의 OAuth2 인가 URL을 제공합니다.") + @ApiResponse(responseCode = "200", description = "URL 생성 성공", + content = @Content(schema = @Schema(implementation = SocialLoginUrlResponse.class))) + ResponseEntity getSocialLoginUrl( + @Parameter(description = "소셜 로그인 제공자", example = "google") String provider, + @Parameter(description = "인가 완료 후 리다이렉트될 URI", example = "https://app.redot.com/auth/callback") String redirectUri, + @Parameter(description = "실패 시 리다이렉트될 URI", example = "https://app.redot.com/auth/fail") String failureUri); +} diff --git a/src/main/java/redot/redot_server/domain/cms/inquiry/controller/RedotAppInquiryController.java b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/RedotAppInquiryController.java index 280ccf1..0b8671c 100644 --- a/src/main/java/redot/redot_server/domain/cms/inquiry/controller/RedotAppInquiryController.java +++ b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/RedotAppInquiryController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.cms.inquiry.controller.docs.RedotAppInquiryControllerDocs; import redot.redot_server.domain.cms.inquiry.dto.request.RedotAppInquiryCreateRequest; import redot.redot_server.domain.cms.inquiry.dto.response.RedotAppInquiryResponse; import redot.redot_server.domain.cms.inquiry.dto.request.RedotAppInquirySearchCondition; @@ -25,23 +26,26 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/app/cms/inquiries") -public class RedotAppInquiryController { +public class RedotAppInquiryController implements RedotAppInquiryControllerDocs { private final RedotAppInquiryService inquiryService; @PostMapping + @Override public ResponseEntity createInquiry(@CurrentRedotApp Long redotAppId, @Valid @RequestBody RedotAppInquiryCreateRequest request) { return ResponseEntity.ok(inquiryService.createInquiry(redotAppId, request)); } @GetMapping("/{inquiryId}") + @Override public ResponseEntity getInquiry(@CurrentRedotApp Long redotAppId, @PathVariable("inquiryId") Long inquiryId) { return ResponseEntity.ok(inquiryService.getInquiry(redotAppId, inquiryId)); } @GetMapping + @Override public ResponseEntity> getAllInquiries( @CurrentRedotApp Long redotAppId, RedotAppInquirySearchCondition searchCondition, @@ -52,6 +56,7 @@ public ResponseEntity> getAllInquiries( } @PatchMapping("/{inquiryId}/complete") + @Override public ResponseEntity markInquiryAsCompleted(@CurrentRedotApp Long redotAppId, @PathVariable("inquiryId") Long inquiryId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { @@ -60,6 +65,7 @@ public ResponseEntity markInquiryAsCompleted(@CurrentRedotApp Long redotAp } @PatchMapping("/{inquiryId}/reopen") + @Override public ResponseEntity reopenInquiry(@CurrentRedotApp Long redotAppId, @PathVariable("inquiryId") Long inquiryId) { inquiryService.reopenInquiry(redotAppId, inquiryId); diff --git a/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java new file mode 100644 index 0000000..d18e23c --- /dev/null +++ b/src/main/java/redot/redot_server/domain/cms/inquiry/controller/docs/RedotAppInquiryControllerDocs.java @@ -0,0 +1,51 @@ +package redot.redot_server.domain.cms.inquiry.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.cms.inquiry.dto.request.RedotAppInquiryCreateRequest; +import redot.redot_server.domain.cms.inquiry.dto.request.RedotAppInquirySearchCondition; +import redot.redot_server.domain.cms.inquiry.dto.response.RedotAppInquiryResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "CMS Inquiry", description = "CMS 문의 관리 API") +public interface RedotAppInquiryControllerDocs { + + @Operation(summary = "문의 생성", description = "CMS 앱에 새 문의를 등록합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = RedotAppInquiryResponse.class))) + ResponseEntity createInquiry(@Parameter(hidden = true) Long redotAppId, + RedotAppInquiryCreateRequest request); + + @Operation(summary = "문의 단건 조회", description = "문의 ID로 상세 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = RedotAppInquiryResponse.class))) + ResponseEntity getInquiry(@Parameter(hidden = true) Long redotAppId, + @Parameter(description = "문의 ID", example = "1") Long inquiryId); + + @Operation(summary = "문의 목록 조회", description = "`status`, `inquiryNumber`, `title`, `inquirerName`, `startDate`, `endDate` 검색 조건과 `page`/`size`/`sort=createdAt,desc` 쿼리 파라미터로 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getAllInquiries(@Parameter(hidden = true) Long redotAppId, + @ParameterObject RedotAppInquirySearchCondition searchCondition, + @Parameter(description = "기본 정렬은 createdAt DESC 입니다.") + @ParameterObject Pageable pageable); + + @Operation(summary = "문의 처리 완료", description = "문의 상태를 완료로 변경합니다.") + @ApiResponse(responseCode = "200", description = "상태 변경 완료") + ResponseEntity markInquiryAsCompleted(@Parameter(hidden = true) Long redotAppId, + @Parameter(description = "문의 ID", example = "1") Long inquiryId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "문의 재오픈", description = "완료된 문의를 다시 열어 진행합니다.") + @ApiResponse(responseCode = "200", description = "재오픈 완료") + ResponseEntity reopenInquiry(@Parameter(hidden = true) Long redotAppId, + @Parameter(description = "문의 ID", example = "1") Long inquiryId); +} diff --git a/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java b/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java index 1bb4f4d..e466b15 100644 --- a/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java +++ b/src/main/java/redot/redot_server/domain/cms/member/controller/CMSMemberController.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.cms.member.controller.docs.CMSMemberControllerDocs; import redot.redot_server.domain.cms.member.dto.request.CMSMemberCreateRequest; import redot.redot_server.domain.cms.member.dto.request.CMSMemberRoleRequest; import redot.redot_server.domain.cms.member.dto.request.CMSMemberSearchCondition; @@ -40,24 +41,27 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/app/cms/members") -public class CMSMemberController { +public class CMSMemberController implements CMSMemberControllerDocs { private final CMSMemberService cmsMemberService; private final TokenCookieFactory tokenCookieFactory; @PostMapping + @Override public ResponseEntity createCMSMember(@CurrentRedotApp Long redotAppId, @RequestBody @Valid CMSMemberCreateRequest request) { return ResponseEntity.ok(cmsMemberService.createCMSMember(redotAppId, request)); } @GetMapping("/{memberId}") + @Override public ResponseEntity getCMSMemberInfo(@CurrentRedotApp Long redotAppId, @PathVariable(name = "memberId") Long memberId) { return ResponseEntity.ok(cmsMemberService.getCMSMemberInfo(redotAppId, memberId)); } @GetMapping + @Override public ResponseEntity> getCMSMemberList(@CurrentRedotApp Long redotAppId, CMSMemberSearchCondition searchCondition, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { @@ -66,6 +70,7 @@ public ResponseEntity> getCMSMemberList(@Current } @PatchMapping("/role/{memberId}") + @Override public ResponseEntity changeCMSMemberRole(@CurrentRedotApp Long redotAppId, @PathVariable(name = "memberId") Long memberId, @RequestBody @Valid CMSMemberRoleRequest request) { @@ -73,6 +78,7 @@ public ResponseEntity changeCMSMemberRole(@CurrentRedotApp Lo } @PutMapping + @Override public ResponseEntity updateCMSMember(@CurrentRedotApp Long redotAppId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @RequestBody @Valid CMSMemberUpdateRequest request) { @@ -80,6 +86,7 @@ public ResponseEntity updateCMSMember(@CurrentRedotApp Long r } @DeleteMapping("/{memberId}") + @Override public ResponseEntity deleteCMSMember(@CurrentRedotApp Long redotAppId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @PathVariable(name = "memberId") Long memberId) { @@ -92,6 +99,7 @@ public ResponseEntity deleteCMSMember(@CurrentRedotApp Long redotAppId, } @DeleteMapping + @Override public ResponseEntity deleteCurrentCMSMember(HttpServletRequest request, @CurrentRedotApp Long redotAppId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { @@ -108,6 +116,7 @@ public ResponseEntity deleteCurrentCMSMember(HttpServletRequest request, } @PostMapping("/upload-profile-image") + @Override public ResponseEntity uploadProfileImage( @CurrentRedotApp Long redotAppId, @AuthenticationPrincipal JwtPrincipal jwtPrincipal, diff --git a/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java new file mode 100644 index 0000000..b898753 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/cms/member/controller/docs/CMSMemberControllerDocs.java @@ -0,0 +1,78 @@ +package redot.redot_server.domain.cms.member.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.cms.member.dto.request.CMSMemberCreateRequest; +import redot.redot_server.domain.cms.member.dto.request.CMSMemberRoleRequest; +import redot.redot_server.domain.cms.member.dto.request.CMSMemberSearchCondition; +import redot.redot_server.domain.cms.member.dto.request.CMSMemberUpdateRequest; +import redot.redot_server.domain.cms.member.dto.response.CMSMemberResponse; +import redot.redot_server.global.s3.dto.UploadedImageUrlResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "CMS Member", description = "CMS 멤버 관리 API") +public interface CMSMemberControllerDocs { + + @Operation(summary = "CMS 멤버 생성", description = "CMS 앱에 새로운 멤버를 추가합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) + ResponseEntity createCMSMember(@Parameter(hidden = true) Long redotAppId, + CMSMemberCreateRequest request); + + @Operation(summary = "CMS 멤버 단건 조회", description = "멤버 ID 기준으로 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) + ResponseEntity getCMSMemberInfo(@Parameter(hidden = true) Long redotAppId, + @Parameter(description = "멤버 ID", example = "1") Long memberId); + + @Operation(summary = "CMS 멤버 목록 조회", description = "`name`, `email`, `role` 검색 조건과 `page`/`size`/`sort=createdAt,desc` 파라미터로 멤버를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getCMSMemberList(@Parameter(hidden = true) Long redotAppId, + @ParameterObject CMSMemberSearchCondition searchCondition, + @Parameter(description = "기본 정렬 createdAt DESC") + @ParameterObject Pageable pageable); + + @Operation(summary = "CMS 멤버 권한 변경", description = "멤버 권한을 관리자/멤버로 변경합니다.") + @ApiResponse(responseCode = "200", description = "변경 성공", + content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) + ResponseEntity changeCMSMemberRole(@Parameter(hidden = true) Long redotAppId, + @Parameter(description = "멤버 ID", example = "1") Long memberId, + CMSMemberRoleRequest request); + + @Operation(summary = "CMS 멤버 정보 수정", description = "본인 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = CMSMemberResponse.class))) + ResponseEntity updateCMSMember(@Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + CMSMemberUpdateRequest request); + + @Operation(summary = "CMS 멤버 삭제", description = "특정 멤버를 삭제합니다.") + @ApiResponse(responseCode = "204", description = "삭제 완료") + ResponseEntity deleteCMSMember(@Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + @Parameter(description = "멤버 ID", example = "1") Long memberId); + + @Operation(summary = "현재 CMS 멤버 탈퇴", description = "본인 계정을 삭제하고 토큰 쿠키를 제거합니다.") + @ApiResponse(responseCode = "204", description = "탈퇴 완료") + ResponseEntity deleteCurrentCMSMember(@Parameter(hidden = true) HttpServletRequest request, + @Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "CMS 멤버 프로필 이미지 업로드", description = "CMS 멤버 이미지 파일을 업로드합니다.") + @ApiResponse(responseCode = "200", description = "업로드 성공", + content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) + ResponseEntity uploadProfileImage(@Parameter(hidden = true) Long redotAppId, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal, + MultipartFile image); +} diff --git a/src/main/java/redot/redot_server/domain/cms/site/menu/controller/CMSMenuController.java b/src/main/java/redot/redot_server/domain/cms/site/menu/controller/CMSMenuController.java index fb54f5a..9cfd6ad 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/menu/controller/CMSMenuController.java +++ b/src/main/java/redot/redot_server/domain/cms/site/menu/controller/CMSMenuController.java @@ -1,29 +1,28 @@ package redot.redot_server.domain.cms.site.menu.controller; -import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.cms.site.menu.controller.docs.CMSMenuControllerDocs; import redot.redot_server.domain.cms.site.menu.dto.response.CMSMenuResponse; import redot.redot_server.domain.cms.site.menu.service.CMSMenuService; import redot.redot_server.domain.redot.app.service.RedotAppService; import redot.redot_server.global.redotapp.resolver.annotation.CurrentRedotApp; -@Tag(name = "CMS Menu", description = "CMS 메뉴 관리 API") @RestController @RequestMapping("/api/v1/app/cms/menus") @RequiredArgsConstructor -public class CMSMenuController { +public class CMSMenuController implements CMSMenuControllerDocs { private final CMSMenuService cmsMenuService; private final RedotAppService redotAppService; @GetMapping + @Override public List getMenus(@CurrentRedotApp Long redotAppId) { Long planId = redotAppService.getRedotAppInfo(redotAppId).redotApp().planId(); return cmsMenuService.getMenusByPlanId(planId); } } - diff --git a/src/main/java/redot/redot_server/domain/cms/site/menu/controller/docs/CMSMenuControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/menu/controller/docs/CMSMenuControllerDocs.java new file mode 100644 index 0000000..5ec4a20 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/cms/site/menu/controller/docs/CMSMenuControllerDocs.java @@ -0,0 +1,20 @@ +package redot.redot_server.domain.cms.site.menu.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import redot.redot_server.domain.cms.site.menu.dto.response.CMSMenuResponse; + +@Tag(name = "CMS Menu", description = "CMS 메뉴 관리 API") +public interface CMSMenuControllerDocs { + + @Operation(summary = "CMS 메뉴 목록 조회", description = "선택한 앱의 메뉴 구성을 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = CMSMenuResponse.class)))) + List getMenus(@Parameter(hidden = true) Long redotAppId); +} diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java index 35fb764..57d8162 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/SiteSettingController.java @@ -1,5 +1,6 @@ package redot.redot_server.domain.cms.site.setting.controller; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.cms.site.setting.controller.docs.SiteSettingControllerDocs; import redot.redot_server.domain.cms.site.setting.dto.response.SiteSettingResponse; import redot.redot_server.domain.cms.site.setting.dto.request.SiteSettingUpdateRequest; import redot.redot_server.domain.cms.site.setting.service.SiteSettingService; @@ -22,20 +24,22 @@ @RequiredArgsConstructor @Validated @RequestMapping("/api/v1/app/cms/site-setting") -public class SiteSettingController { +public class SiteSettingController implements SiteSettingControllerDocs { private final SiteSettingService siteSettingService; @PutMapping + @Override public ResponseEntity updateSiteSetting( @CurrentRedotApp Long redotAppId, - @RequestBody SiteSettingUpdateRequest request + @RequestBody @Valid SiteSettingUpdateRequest request ) { SiteSettingResponse siteSettingResponse = siteSettingService.updateSiteSetting(redotAppId, request); return ResponseEntity.ok(siteSettingResponse); } @PostMapping("/upload-logo") + @Override public ResponseEntity uploadLogoImage( @CurrentRedotApp Long redotAppId, @RequestPart("logo") @NotNull(message = "업로드할 로고 파일을 선택해주세요.") MultipartFile logoFile @@ -44,6 +48,7 @@ public ResponseEntity uploadLogoImage( } @GetMapping + @Override public ResponseEntity getSiteSetting( @CurrentRedotApp Long redotAppId ) { diff --git a/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java new file mode 100644 index 0000000..dd1480e --- /dev/null +++ b/src/main/java/redot/redot_server/domain/cms/site/setting/controller/docs/SiteSettingControllerDocs.java @@ -0,0 +1,34 @@ +package redot.redot_server.domain.cms.site.setting.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.cms.site.setting.dto.request.SiteSettingUpdateRequest; +import redot.redot_server.domain.cms.site.setting.dto.response.SiteSettingResponse; +import redot.redot_server.global.s3.dto.UploadedImageUrlResponse; + +@Tag(name = "Site Setting", description = "사이트 설정 API") +public interface SiteSettingControllerDocs { + + @Operation(summary = "사이트 설정 수정", description = "CMS 앱의 사이트 기본 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = SiteSettingResponse.class))) + ResponseEntity updateSiteSetting(@Parameter(hidden = true) Long redotAppId, + SiteSettingUpdateRequest request); + + @Operation(summary = "사이트 로고 업로드", description = "사이트 로고 이미지를 업로드합니다.") + @ApiResponse(responseCode = "200", description = "업로드 성공", + content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) + ResponseEntity uploadLogoImage(@Parameter(hidden = true) Long redotAppId, + MultipartFile logoFile); + + @Operation(summary = "사이트 설정 조회", description = "현재 CMS 앱의 사이트 설정을 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = SiteSettingResponse.class))) + ResponseEntity getSiteSetting(@Parameter(hidden = true) Long redotAppId); +} diff --git a/src/main/java/redot/redot_server/domain/cms/site/style/controller/StyleInfoController.java b/src/main/java/redot/redot_server/domain/cms/site/style/controller/StyleInfoController.java index af7c18f..75c6157 100644 --- a/src/main/java/redot/redot_server/domain/cms/site/style/controller/StyleInfoController.java +++ b/src/main/java/redot/redot_server/domain/cms/site/style/controller/StyleInfoController.java @@ -1,5 +1,6 @@ package redot.redot_server.domain.cms.site.style.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -7,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.cms.site.style.controller.docs.StyleInfoControllerDocs; import redot.redot_server.domain.cms.site.style.dto.response.StyleInfoResponse; import redot.redot_server.domain.cms.site.style.dto.request.StyleInfoUpdateRequest; import redot.redot_server.domain.cms.site.style.service.StyleInfoService; @@ -15,17 +17,19 @@ @RestController @RequestMapping("/api/v1/app/cms/style-info") @RequiredArgsConstructor -public class StyleInfoController { +public class StyleInfoController implements StyleInfoControllerDocs { private final StyleInfoService styleInfoService; @GetMapping + @Override public ResponseEntity getStyleInfo(@CurrentRedotApp Long redotAppId) { StyleInfoResponse styleInfoResponse = styleInfoService.getStyleInfo(redotAppId); return ResponseEntity.ok(styleInfoResponse); } @PatchMapping - public ResponseEntity updateStyleInfo(@CurrentRedotApp Long redotAppId, @RequestBody StyleInfoUpdateRequest request) { + @Override + public ResponseEntity updateStyleInfo(@CurrentRedotApp Long redotAppId, @RequestBody @Valid StyleInfoUpdateRequest request) { return ResponseEntity.ok().body(styleInfoService.updateStyleInfo(redotAppId, request)); } } diff --git a/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java b/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java new file mode 100644 index 0000000..5d7ef60 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/cms/site/style/controller/docs/StyleInfoControllerDocs.java @@ -0,0 +1,26 @@ +package redot.redot_server.domain.cms.site.style.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.cms.site.style.dto.request.StyleInfoUpdateRequest; +import redot.redot_server.domain.cms.site.style.dto.response.StyleInfoResponse; + +@Tag(name = "Style Info", description = "스타일 정보 API") +public interface StyleInfoControllerDocs { + + @Operation(summary = "스타일 정보 조회", description = "CMS 앱의 스타일 테마 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = StyleInfoResponse.class))) + ResponseEntity getStyleInfo(@Parameter(hidden = true) Long redotAppId); + + @Operation(summary = "스타일 정보 수정", description = "스타일 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = StyleInfoResponse.class))) + ResponseEntity updateStyleInfo(@Parameter(hidden = true) Long redotAppId, + StyleInfoUpdateRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/redot/app/controller/RedotAppController.java b/src/main/java/redot/redot_server/domain/redot/app/controller/RedotAppController.java index d186f9f..9b982f9 100644 --- a/src/main/java/redot/redot_server/domain/redot/app/controller/RedotAppController.java +++ b/src/main/java/redot/redot_server/domain/redot/app/controller/RedotAppController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; +import redot.redot_server.domain.redot.app.controller.docs.RedotAppControllerDocs; import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateManagerRequest; import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest; import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse; @@ -25,17 +26,19 @@ @RestController @RequestMapping("/api/v1/app") @RequiredArgsConstructor -public class RedotAppController { +public class RedotAppController implements RedotAppControllerDocs { private final RedotAppService redotAppService; // subdomain 기반 앱 정보 조회 @GetMapping("/by-subdomain") + @Override public ResponseEntity getRedotAppInfo(@CurrentRedotApp Long redotAppId) { return ResponseEntity.ok(redotAppService.getRedotAppInfo(redotAppId)); } @GetMapping + @Override public ResponseEntity> getRedotAppList( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { @@ -43,12 +46,14 @@ public ResponseEntity> getRedotAppList( } @PostMapping + @Override public ResponseEntity createRedotApp(@Valid @RequestBody RedotAppCreateRequest request, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { return ResponseEntity.ok(redotAppService.createRedotApp(request, jwtPrincipal.id())); } @PostMapping("/{redotAppId}/create-manager") + @Override public ResponseEntity createManager(@PathVariable("redotAppId") Long redotAppId, @Valid @RequestBody RedotAppCreateManagerRequest request, @AuthenticationPrincipal JwtPrincipal jwtPrincipal) { diff --git a/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java new file mode 100644 index 0000000..601271f --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/app/controller/docs/RedotAppControllerDocs.java @@ -0,0 +1,44 @@ +package redot.redot_server.domain.redot.app.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springdoc.core.annotations.ParameterObject; +import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateManagerRequest; +import redot.redot_server.domain.redot.app.dto.request.RedotAppCreateRequest; +import redot.redot_server.domain.redot.app.dto.response.RedotAppInfoResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; +import redot.redot_server.global.util.dto.response.PageResponse; + +@Tag(name = "Redot App", description = "Redot 앱 API") +public interface RedotAppControllerDocs { + + @Operation(summary = "앱 기본 정보 조회", description = "현재 도메인에 매핑된 Redot 앱 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class))) + ResponseEntity getRedotAppInfo(@Parameter(hidden = true) Long redotAppId); + + @Operation(summary = "앱 목록 조회", description = "`page`, `size`, `sort=createdAt,desc` 파라미터와 함께 요청하면 자신이 속한 앱 목록을 최신순으로 확인할 수 있습니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = PageResponse.class))) + ResponseEntity> getRedotAppList(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, + @Parameter(description = "기본 정렬 createdAt DESC") + @ParameterObject Pageable pageable); + + @Operation(summary = "앱 생성", description = "새로운 Redot 앱을 생성합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = RedotAppInfoResponse.class))) + ResponseEntity createRedotApp(RedotAppCreateRequest request, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); + + @Operation(summary = "앱 매니저 생성", description = "Redot 앱에 추가 매니저를 초대합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공") + ResponseEntity createManager(@Parameter(description = "Redot 앱 ID", example = "1") Long redotAppId, + RedotAppCreateManagerRequest request, + @Parameter(hidden = true) JwtPrincipal jwtPrincipal); +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/config/ConsultationNotificationProperties.java b/src/main/java/redot/redot_server/domain/redot/consultation/config/ConsultationNotificationProperties.java new file mode 100644 index 0000000..d54fec3 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/consultation/config/ConsultationNotificationProperties.java @@ -0,0 +1,11 @@ +package redot.redot_server.domain.redot.consultation.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "notifications.consultation") +public record ConsultationNotificationProperties( + String adminConsoleBaseUrl, + Discord discord +) { + public record Discord(String webhookUrl) {} +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/controller/ConsultationController.java b/src/main/java/redot/redot_server/domain/redot/consultation/controller/ConsultationController.java index f5e9870..2c66496 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/controller/ConsultationController.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/controller/ConsultationController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.redot.consultation.controller.docs.ConsultationControllerDocs; import redot.redot_server.domain.redot.consultation.dto.request.ConsultationCreateRequest; import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; import redot.redot_server.domain.redot.consultation.service.ConsultationService; @@ -14,11 +15,12 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/redot/consultations") -public class ConsultationController { +public class ConsultationController implements ConsultationControllerDocs { private final ConsultationService consultationService; @PostMapping + @Override public ResponseEntity createConsultation(@RequestBody @Valid ConsultationCreateRequest request) { ConsultationResponse response = consultationService.createConsultation(request); return ResponseEntity.ok(response); diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java new file mode 100644 index 0000000..0e98acb --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/consultation/controller/docs/ConsultationControllerDocs.java @@ -0,0 +1,19 @@ +package redot.redot_server.domain.redot.consultation.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.redot.consultation.dto.request.ConsultationCreateRequest; +import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; + +@Tag(name = "Consultation", description = "상담 신청 API") +public interface ConsultationControllerDocs { + + @Operation(summary = "상담 신청 생성", description = "서비스 상담 신청을 등록합니다.") + @ApiResponse(responseCode = "200", description = "생성 성공", + content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) + ResponseEntity createConsultation(ConsultationCreateRequest request); +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationChannel.java b/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationChannel.java new file mode 100644 index 0000000..903146a --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationChannel.java @@ -0,0 +1,8 @@ +package redot.redot_server.domain.redot.consultation.notification; + +import redot.redot_server.domain.redot.consultation.entity.Consultation; + +public interface ConsultationNotificationChannel { + + void send(Consultation consultation); +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationService.java b/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationService.java new file mode 100644 index 0000000..f8f07a2 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/consultation/notification/ConsultationNotificationService.java @@ -0,0 +1,25 @@ +package redot.redot_server.domain.redot.consultation.notification; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import redot.redot_server.domain.redot.consultation.entity.Consultation; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ConsultationNotificationService { + + private final List channels; + + public void sendConsultationCreated(Consultation consultation) { + for (ConsultationNotificationChannel channel : channels) { + try { + channel.send(consultation); + } catch (Exception ex) { + log.error("Failed to send consultation notification via {}", channel.getClass().getSimpleName(), ex); + } + } + } +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/notification/DiscordConsultationNotificationChannel.java b/src/main/java/redot/redot_server/domain/redot/consultation/notification/DiscordConsultationNotificationChannel.java new file mode 100644 index 0000000..70991ae --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/consultation/notification/DiscordConsultationNotificationChannel.java @@ -0,0 +1,133 @@ +package redot.redot_server.domain.redot.consultation.notification; + +import jakarta.annotation.PostConstruct; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import redot.redot_server.domain.redot.consultation.config.ConsultationNotificationProperties; +import redot.redot_server.domain.redot.consultation.entity.Consultation; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DiscordConsultationNotificationChannel implements ConsultationNotificationChannel { + + private static final DateTimeFormatter CREATED_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + private final RestTemplateBuilder restTemplateBuilder; + private final ConsultationNotificationProperties properties; + private RestTemplate restTemplate; + + @PostConstruct + void initRestTemplate() { + this.restTemplate = restTemplateBuilder + .requestFactory(this::createRequestFactory) + .build(); + } + + private ClientHttpRequestFactory createRequestFactory() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + return factory; + } + + @Override + public void send(Consultation consultation) { + ConsultationNotificationProperties.Discord discord = properties.discord(); + if (discord == null || !StringUtils.hasText(discord.webhookUrl())) { + return; + } + + Map payload = buildPayload(consultation); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(payload, headers); + + try { + restTemplate.postForEntity(discord.webhookUrl(), entity, Void.class); + } catch (Exception ex) { + log.error("Failed to send Discord consultation notification", ex); + } + } + + private Map buildPayload(Consultation consultation) { + Map payload = new HashMap<>(); + payload.put("content", "📩 **새 상담이 접수되었습니다.**"); + + Map embed = new HashMap<>(); + embed.put("title", "Consultation #" + consultation.getId()); + embed.put("url", buildConsultationUrl(consultation)); + embed.put("description", "유형: **" + translateType(consultation) + "**"); + embed.put("color", 0x3498DB); + + List> fields = new ArrayList<>(); + fields.add(field("이메일 ✉️", consultation.getEmail(), true)); + fields.add(field("연락처 📞", consultation.getPhone(), true)); + fields.add(field("기존 웹사이트 주소 🌐", consultation.getCurrentWebsiteUrl(), true)); + fields.add(field("상담 관리 🔗", buildConsultationUrl(consultation), true)); + fields.add(field("내용", formatContent(consultation.getContent()), false)); + embed.put("fields", fields); + + Map footer = new HashMap<>(); + footer.put("text", "생성일시: " + formatCreatedAt(consultation)); + embed.put("footer", footer); + + payload.put("embeds", List.of(embed)); + return payload; + } + + private Map field(String name, String value, boolean inline) { + Map field = new HashMap<>(); + field.put("name", name); + field.put("value", StringUtils.hasText(value) ? value : "-"); + field.put("inline", inline); + return field; + } + + private String buildConsultationUrl(Consultation consultation) { + if (!StringUtils.hasText(properties.adminConsoleBaseUrl())) { + return null; + } + return properties.adminConsoleBaseUrl() + "/consultation?id=" + consultation.getId(); + } + + private String formatCreatedAt(Consultation consultation) { + if (consultation.getCreatedAt() == null) { + return "-"; + } + return CREATED_AT_FORMATTER.format(consultation.getCreatedAt()); + } + + private String formatContent(String content) { + if (!StringUtils.hasText(content)) { + return "-"; + } + if (content.length() > 1000) { + content = content.substring(0, 997) + "..."; + } + return "```" + content + "```"; + } + + private String translateType(Consultation consultation) { + return switch (consultation.getType()) { + case NEW -> "신규 제작"; + case RENEWAL -> "리뉴얼"; + }; + } + +} diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/service/ConsultationService.java b/src/main/java/redot/redot_server/domain/redot/consultation/service/ConsultationService.java index e9300e4..2c8617a 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/service/ConsultationService.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/service/ConsultationService.java @@ -6,6 +6,7 @@ import redot.redot_server.domain.redot.consultation.dto.request.ConsultationCreateRequest; import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; import redot.redot_server.domain.redot.consultation.entity.Consultation; +import redot.redot_server.domain.redot.consultation.notification.ConsultationNotificationService; import redot.redot_server.domain.redot.consultation.repository.ConsultationRepository; @Service @@ -14,12 +15,15 @@ public class ConsultationService { private final ConsultationRepository consultationRepository; + private final ConsultationNotificationService consultationNotificationService; @Transactional public ConsultationResponse createConsultation(ConsultationCreateRequest request) { Consultation saved = consultationRepository.save(Consultation.create(request)); + consultationNotificationService.sendConsultationCreated(saved); + return ConsultationResponse.fromEntity(saved); } } diff --git a/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java b/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java index c11a885..26b07c5 100644 --- a/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java +++ b/src/main/java/redot/redot_server/domain/redot/member/controller/RedotMemberController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.redot.member.controller.docs.RedotMemberControllerDocs; import redot.redot_server.domain.redot.member.dto.RedotMemberUpdateRequest; import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse; import redot.redot_server.domain.redot.member.service.RedotMemberService; @@ -21,12 +22,13 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/redot/members") -public class RedotMemberController { +public class RedotMemberController implements RedotMemberControllerDocs { private final RedotMemberService redotMemberService; @PutMapping + @Override public ResponseEntity updateRedotMemberInfo( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @RequestBody RedotMemberUpdateRequest request @@ -37,6 +39,7 @@ public ResponseEntity updateRedotMemberInfo( @PostMapping("/upload-profile-image") + @Override public ResponseEntity uploadProfileImage( @AuthenticationPrincipal JwtPrincipal jwtPrincipal, @RequestPart("image") @NotNull MultipartFile image @@ -45,6 +48,7 @@ public ResponseEntity uploadProfileImage( } @DeleteMapping + @Override public ResponseEntity deleteRedotMember( @AuthenticationPrincipal JwtPrincipal jwtPrincipal ) { diff --git a/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java new file mode 100644 index 0000000..649b90f --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/member/controller/docs/RedotMemberControllerDocs.java @@ -0,0 +1,34 @@ +package redot.redot_server.domain.redot.member.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; +import redot.redot_server.domain.redot.member.dto.RedotMemberUpdateRequest; +import redot.redot_server.domain.redot.member.dto.response.RedotMemberResponse; +import redot.redot_server.global.s3.dto.UploadedImageUrlResponse; +import redot.redot_server.global.security.principal.JwtPrincipal; + +@Tag(name = "Redot Member", description = "Redot 회원 API") +public interface RedotMemberControllerDocs { + + @Operation(summary = "Redot 회원 정보 수정", description = "회원 프로필 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "수정 성공", + content = @Content(schema = @Schema(implementation = RedotMemberResponse.class))) + ResponseEntity updateRedotMemberInfo(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, + RedotMemberUpdateRequest request); + + @Operation(summary = "Redot 회원 프로필 이미지 업로드", description = "회원 프로필 이미지를 업로드합니다.") + @ApiResponse(responseCode = "200", description = "업로드 성공", + content = @Content(schema = @Schema(implementation = UploadedImageUrlResponse.class))) + ResponseEntity uploadProfileImage(@Parameter(hidden = true) JwtPrincipal jwtPrincipal, + MultipartFile image); + + @Operation(summary = "Redot 회원 탈퇴", description = "회원 계정을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "탈퇴 완료") + ResponseEntity deleteRedotMember(@Parameter(hidden = true) JwtPrincipal jwtPrincipal); +} diff --git a/src/main/java/redot/redot_server/domain/redot/plan/controller/PlanController.java b/src/main/java/redot/redot_server/domain/redot/plan/controller/PlanController.java index d9087c3..65654a8 100644 --- a/src/main/java/redot/redot_server/domain/redot/plan/controller/PlanController.java +++ b/src/main/java/redot/redot_server/domain/redot/plan/controller/PlanController.java @@ -1,23 +1,23 @@ package redot.redot_server.domain.redot.plan.controller; -import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.redot.plan.controller.docs.PlanControllerDocs; import redot.redot_server.domain.redot.plan.dto.response.PlanResponse; import redot.redot_server.domain.redot.plan.service.PlanService; -@Tag(name = "Plan", description = "플랜 관리 API") @RestController @RequestMapping("/api/v1/app/plans") @RequiredArgsConstructor -public class PlanController { +public class PlanController implements PlanControllerDocs { private final PlanService planService; @GetMapping + @Override public List getAllPlans() { return planService.getAllPlans(); } diff --git a/src/main/java/redot/redot_server/domain/redot/plan/controller/docs/PlanControllerDocs.java b/src/main/java/redot/redot_server/domain/redot/plan/controller/docs/PlanControllerDocs.java new file mode 100644 index 0000000..9ea635d --- /dev/null +++ b/src/main/java/redot/redot_server/domain/redot/plan/controller/docs/PlanControllerDocs.java @@ -0,0 +1,19 @@ +package redot.redot_server.domain.redot.plan.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import redot.redot_server.domain.redot.plan.dto.response.PlanResponse; + +@Tag(name = "Plan", description = "플랜 관리 API") +public interface PlanControllerDocs { + + @Operation(summary = "플랜 목록 조회", description = "이용 가능한 모든 플랜 정보를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = PlanResponse.class)))) + List getAllPlans(); +} diff --git a/src/main/java/redot/redot_server/domain/site/domain/controller/DomainController.java b/src/main/java/redot/redot_server/domain/site/domain/controller/DomainController.java index db31d61..6daac1a 100644 --- a/src/main/java/redot/redot_server/domain/site/domain/controller/DomainController.java +++ b/src/main/java/redot/redot_server/domain/site/domain/controller/DomainController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import redot.redot_server.domain.site.domain.controller.docs.DomainControllerDocs; import redot.redot_server.domain.site.domain.dto.request.SubdomainLookupRequest; import redot.redot_server.domain.site.domain.dto.response.SubdomainLookupResponse; import redot.redot_server.domain.site.domain.service.DomainService; @@ -14,11 +15,12 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/v1/domain") -public class DomainController { +public class DomainController implements DomainControllerDocs { private final DomainService domainService; @PostMapping("/subdomain") + @Override public ResponseEntity getSubdomain(@Valid @RequestBody SubdomainLookupRequest request) { return ResponseEntity.ok(domainService.getSubdomain(request)); diff --git a/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java b/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java new file mode 100644 index 0000000..7852ea6 --- /dev/null +++ b/src/main/java/redot/redot_server/domain/site/domain/controller/docs/DomainControllerDocs.java @@ -0,0 +1,19 @@ +package redot.redot_server.domain.site.domain.controller.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import redot.redot_server.domain.site.domain.dto.request.SubdomainLookupRequest; +import redot.redot_server.domain.site.domain.dto.response.SubdomainLookupResponse; + +@Tag(name = "Domain", description = "도메인 API") +public interface DomainControllerDocs { + + @Operation(summary = "서브도메인 조회", description = "사용 가능한 서브도메인인지 확인합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공", + content = @Content(schema = @Schema(implementation = SubdomainLookupResponse.class))) + ResponseEntity getSubdomain(SubdomainLookupRequest request); +}