From 7fd59a8ec944d2246ca906cdacfa16d6631d77c5 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 15 Mar 2025 12:56:12 +0900 Subject: [PATCH 01/70] =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D,=20=EC=82=AC=EC=9A=A9=EC=9E=90=20repo=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20http?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- http/github-oauth2.http | 10 ++++++++++ http/member-repo.http | 12 ++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 http/github-oauth2.http create mode 100644 http/member-repo.http diff --git a/http/github-oauth2.http b/http/github-oauth2.http new file mode 100644 index 0000000..5c4801f --- /dev/null +++ b/http/github-oauth2.http @@ -0,0 +1,10 @@ +### 깃허브 인증 페이지 리다이렉트 +GET http://localhost:8080/api/v1/login/oauth/github + +### 깃허브 인증 코드로 github access token 발급 +POST http://localhost:8080/api/v1/login/oauth/github/tokens +Content-Type: application/json + +{ + "code": "3ad1cb50aa501f66faca" +} diff --git a/http/member-repo.http b/http/member-repo.http new file mode 100644 index 0000000..8c5e3fb --- /dev/null +++ b/http/member-repo.http @@ -0,0 +1,12 @@ +### 사용자 repo 생성 +POST http://localhost:8080/api/v1/member/repo +Content-Type: application/json +Authorization: Bearer jwt + +{ + "name": "gitmon" +} + +### 사용자 repo 이름 중복 확인 +GET http://localhost:8080/api/v1/member/repo/check?name=gitmon +Authorization: Bearer jwt From b4b1c100bb99743802715b5b03acbb043e2c5760 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 15 Mar 2025 13:00:48 +0900 Subject: [PATCH 02/70] =?UTF-8?q?JwtAuthInterceptor=20=EA=B5=AC=ED=98=84,?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../common/const/RequestHeaderAttributes.kt | 3 ++ .../ixfp/gitmon/config/JwtAuthInterceptor.kt | 53 +++++++++++++++++++ .../com/ixfp/gitmon/config/WebMvcConfig.kt | 15 ++++++ .../gitmon/controller/MemberController.kt | 35 ++++-------- .../gitmon/controller/PostingController.kt | 24 ++------- 5 files changed, 85 insertions(+), 45 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt b/src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt new file mode 100644 index 0000000..3948b74 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt @@ -0,0 +1,3 @@ +package com.ixfp.gitmon.common.const + +const val AUTHENTICATED_MEMBER = "authenticatedMember" diff --git a/src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt b/src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt new file mode 100644 index 0000000..119e316 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt @@ -0,0 +1,53 @@ +package com.ixfp.gitmon.config + +import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER +import com.ixfp.gitmon.common.util.JwtUtil +import com.ixfp.gitmon.domain.auth.AuthService +import io.github.oshai.kotlinlogging.KotlinLogging.logger +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.servlet.HandlerInterceptor + +@Component +class JwtAuthInterceptor( + private val jwtUtil: JwtUtil, + private val authService: AuthService, +) : HandlerInterceptor { + override fun preHandle( + request: HttpServletRequest, + response: HttpServletResponse, + handler: Any, + ): Boolean { + val authHeader = request.getHeader("Authorization") + + if (authHeader.isNullOrBlank() || !authHeader.startsWith("Bearer ")) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header") + return false + } + + val token = authHeader.removePrefix("Bearer ") + val exposedId = + jwtUtil.parseAccessToken(token)?.exposedId + ?: run { + log.info { "유효하지 않은 사용자 토큰" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } + + val member = + authService.getMemberByExposedId(exposedId) + ?: run { + log.info { "토큰에 해당하는 사용자를 찾을 수 없음" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } + + request.setAttribute(AUTHENTICATED_MEMBER, member) // 요청 속성에 저장하여 컨트롤러에서 사용 가능 + return true + } + + companion object { + private val log = logger {} + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt new file mode 100644 index 0000000..26ecb89 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt @@ -0,0 +1,15 @@ +package com.ixfp.gitmon.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebMvcConfig( + private val jwtAuthInterceptor: JwtAuthInterceptor, +) : WebMvcConfigurer { + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(jwtAuthInterceptor) + .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 8f25062..47f25ba 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,8 +1,9 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.util.JwtUtil +import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.domain.auth.AuthService +import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.swagger.v3.oas.annotations.Operation @@ -13,8 +14,8 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -24,7 +25,6 @@ import javax.naming.AuthenticationException @RestController() @Tag(name = "Member", description = "회원 관련 API") class MemberController( - private val jwtUtil: JwtUtil, private val authService: AuthService, private val memberService: MemberService, ) { @@ -59,23 +59,13 @@ class MemberController( @PostMapping("/repo") fun upsertRepo( @RequestBody request: CreateRepoRequest, - @RequestHeader("Authorization") authorizationHeader: String, + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { try { - // TODO: 엑세스토큰으로 id 혹은 member 객체로 변환하는 필터 만들기 - val accessToken = authorizationHeader.removePrefix("Bearer ") - val memberExposedId = jwtUtil.parseAccessToken(accessToken)?.exposedId - if (memberExposedId == null) { - throw AuthenticationException("유효하지 않은 사용자 토큰") - } - val member = authService.getMemberByExposedId(memberExposedId) - if (member == null) { - throw Error("토큰에 해당하는 사용자를 찾을 수 없음") - } - val githubAccessToken = authService.getGithubAccessToken(member.id) - if (githubAccessToken == null) { - throw Error("사용자의 깃허브 토큰을 찾을 수 없음") - } + val githubAccessToken = + authService.getGithubAccessToken(member.id) + ?: throw AuthenticationException("사용자의 깃허브 토큰을 찾을 수 없음") + if (member.repoName == request.name) { return ResponseEntity(HttpStatus.OK) } @@ -109,16 +99,9 @@ class MemberController( @GetMapping("/repo/check") fun checkRepoName( @RequestParam name: String, - @RequestHeader("Authorization") authorizationHeader: String, + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { return try { - val accessToken = authorizationHeader.removePrefix("Bearer ") - val memberExposedId = - jwtUtil.parseAccessToken(accessToken)?.exposedId - ?: throw AuthenticationException("유효하지 않은 사용자 토큰") - val member = - authService.getMemberByExposedId(memberExposedId) - ?: throw Error("토큰에 해당하는 사용자를 찾을 수 없음") val isAvailable = memberService.isRepoNameAvailable(member, name) if (!isAvailable) { ResponseEntity.status(HttpStatus.CONFLICT).build() diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index b55edd4..cb23462 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,46 +1,32 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.util.JwtUtil +import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreatePostingRequest -import com.ixfp.gitmon.domain.auth.AuthService +import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import javax.naming.AuthenticationException @RequestMapping("/api/v1/posting") @RestController class PostingController( - private val jwtUtil: JwtUtil, - private val authService: AuthService, private val postingService: PostingService, ) { @PostMapping fun createPosting( @RequestBody request: CreatePostingRequest, - @RequestHeader("Authorization") authorizationHeader: String + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { try { - val accessToken = authorizationHeader.removePrefix("Bearer ") - val memberExposedId = jwtUtil.parseAccessToken(accessToken)?.exposedId - if (memberExposedId == null) { - throw AuthenticationException("유효하지 않은 사용자 토큰") - } - val member = authService.getMemberByExposedId(memberExposedId) - if (member == null) { - throw Error("토큰에 해당하는 사용자를 찾을 수 없음") - } - postingService.create(member, request.title, request.content) - return ResponseEntity(HttpStatus.CREATED) } catch (e: Exception) { return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } } -} \ No newline at end of file +} From d1ca0f4902bc4dbb83a297088a0e3d23be4ccda5 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 15 Mar 2025 13:10:01 +0900 Subject: [PATCH 03/70] fix lint Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/domain/posting/PostingService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 0602500..841f70c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -8,9 +8,14 @@ import org.springframework.stereotype.Service class PostingService( private val memberReader: MemberReader, ) { - fun create(member: Member, title: String, content: String) { - val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) - ?: throw Error("Github 엑세스 토큰 없음") + fun create( + member: Member, + title: String, + content: String, + ) { + val githubAccessToken = + memberReader.findAccessTokenByMemberId(member.id) + ?: throw Error("Github 엑세스 토큰 없음") // Todo: githubService 연동하여 posting upsert API 호출 } From edcad3186fc15a8460f97b823f4b127090ea8c87 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 16 Mar 2025 19:34:30 +0900 Subject: [PATCH 04/70] =?UTF-8?q?CORS=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt index 26ecb89..7ccde3d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt @@ -1,6 +1,7 @@ package com.ixfp.gitmon.config import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @@ -12,4 +13,18 @@ class WebMvcConfig( registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 } + + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins(*ALLOWED_ORIGINS) + .allowedMethods(*ALLOWED_METHODS) + .allowedHeaders("*") + .allowCredentials(true) + } + + companion object { + private val GITMON_CLIENT_URL = "https://gitmon.blog" + private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL) + private val ALLOWED_METHODS = arrayOf("GET", "POST", "PUT", "DELETE", "OPTIONS") + } } From 4b8e88efd272c2f5f3690acf50f9405767fda58f Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 16 Mar 2025 21:30:35 +0900 Subject: [PATCH 05/70] =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=20name?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=9A=8C=EC=9B=90=EC=9D=98=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=A3=BC=EC=86=8C?= =?UTF-8?q?=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8A=94=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/controller/MemberController.kt | 20 +++++++++++++++++++ .../response/GithubRepoUrlResponse.kt | 5 +++++ .../ixfp/gitmon/db/member/MemberRepository.kt | 2 +- .../ixfp/gitmon/domain/member/MemberReader.kt | 4 ++++ .../gitmon/domain/member/MemberService.kt | 17 ++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/response/GithubRepoUrlResponse.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 47f25ba..bdcf4bc 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -2,6 +2,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest +import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.auth.AuthService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService @@ -120,6 +121,25 @@ class MemberController( } } + @Operation(summary = "레포지토리 URL 조회") + @ApiResponses( + value = [ + ApiResponse(responseCode = "404", description = "레포지토리 URL 찾을 수 없음"), + ], + ) + @GetMapping("/github/repo") + fun findGithubRepoUrl( + @RequestParam githubUsername: String, + ): ResponseEntity { + val githubRepoUrl = + memberService.findGithubRepoUrl(githubUsername) + ?: return ResponseEntity.status(HttpStatus.NOT_FOUND).build() + + val response = GithubRepoUrlResponse(githubRepoUrl) + + return ResponseEntity.status(HttpStatus.OK).body(response) + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/GithubRepoUrlResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/GithubRepoUrlResponse.kt new file mode 100644 index 0000000..011bfdb --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/GithubRepoUrlResponse.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.controller.response + +data class GithubRepoUrlResponse( + val githubRepoUrl: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/db/member/MemberRepository.kt b/src/main/kotlin/com/ixfp/gitmon/db/member/MemberRepository.kt index c516265..f5d649b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/db/member/MemberRepository.kt +++ b/src/main/kotlin/com/ixfp/gitmon/db/member/MemberRepository.kt @@ -10,7 +10,7 @@ interface MemberRepository : JpaRepository { fun findByGithubId(githubId: Long): MemberEntity? - fun findByGithubUsername(githubUsername: String): List + fun findByGithubUsername(githubUsername: String): MemberEntity? fun findByExposedId(exposedId: String): MemberEntity? } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt index 8c65e6f..75b957d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt @@ -24,6 +24,10 @@ class MemberReader( return memberRepository.findByGithubId(githubId)?.let { MemberEntity.toMember(it) } } + fun findByGithubUsername(githubUsername: String): Member? { + return memberRepository.findByGithubUsername(githubUsername)?.let { MemberEntity.toMember(it) } + } + fun findAccessTokenByMemberId(memberId: Long): String? { // TODO: 깃허브 토큰만 조회하게 변경 return githubAuthRepository.findByMemberId(memberId)?.githubAccessToken diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index a7a1dc3..c8b07a4 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -38,6 +38,23 @@ class MemberService( return true } + fun findGithubRepoUrl(githubUsername: String): String? { + val member = memberReader.findByGithubUsername(githubUsername) ?: return null + + if (member.repoName == null) { + return null + } + + return buildGithubRepoUrl(member.githubUsername, member.repoName) + } + + private fun buildGithubRepoUrl( + githubUsername: String, + repoName: String, + ): String { + return "https://github.com/$githubUsername/$repoName" + } + private fun isRepoNameDuplicate( member: Member, repoName: String, From 93aedee5fd910ce2d2504ab7dd7c05d8a78e4537 Mon Sep 17 00:00:00 2001 From: TraceofLight Date: Sat, 22 Mar 2025 02:48:42 +0900 Subject: [PATCH 06/70] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20repo?= =?UTF-8?q?=EC=97=90=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 작성한 내용을 파일로 받아 이를 github에 commit의 형태로 전달하는 기능 구현 - member controller 쪽 api 호출부 구현 - base64 활용한 인코딩 처리 오브젝트 구현 - github auth 정보와 조합하여 파일 업로드하는 기능 추가 --- .../gitmon/client/github/GithubApiService.kt | 30 +++++++++++-- .../client/github/GithubResourceApiClient.kt | 18 ++++++++ .../github/request/GithubUpsertFileRequest.kt | 7 ++++ .../response/GithubUpsertFileResponse.kt | 5 +++ .../ixfp/gitmon/common/util/Base64Encoder.kt | 10 +++++ .../gitmon/controller/MemberController.kt | 42 +++++++++++++++++++ 6 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 458a9b6..fdf1473 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -2,10 +2,13 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest +import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubUserResponse +import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile @Component class GithubApiService( @@ -28,9 +31,30 @@ class GithubApiService( return githubOauth2ApiClient.fetchAccessToken(request).accessToken } - // TODO(KHJ): dummy function, 관련 api 나오면 정리할 것 - fun hasRepository(githubAccessToken: String): Boolean { - return false + fun upsertFile( + githubAccessToken: String, + content: MultipartFile, + repo: String, + path: String, + commitMessage: String = "Upsert File by API", + ): String { + val request = + GithubUpsertFileRequest( + message = "Add New File", + content = Base64Encoder.encodeBase64(content), + sha = "", + ) + val owner = "traceoflight" // TODO(KHJ): db에서 owner name 가져오는 부분 대응할 것 + val response = + githubResourceApiClient.upsertFile( + bearerToken = BearerToken(githubAccessToken).format(), + owner = owner, + repo = repo, + path = path, + request = request, + ) + + return response.sha } private fun createRepository( diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt index ed2c596..63d532b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt @@ -1,14 +1,17 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest +import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubCreateRepositoryResponse import com.ixfp.gitmon.client.github.response.GithubFetchRepositoryResponse +import com.ixfp.gitmon.client.github.response.GithubUpsertFileResponse import com.ixfp.gitmon.client.github.response.GithubUserResponse import org.springframework.cloud.openfeign.FeignClient import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader @@ -47,4 +50,19 @@ interface GithubResourceApiClient { @RequestHeader("Accept") accept: String = "application/vnd.github+json", @RequestHeader("X-GitHub-Api-Version") apiVersion: String = "2022-11-28", ): GithubCreateRepositoryResponse + + @PutMapping( + "/repos/{owner}/{repo}/contents/{path}", + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun upsertFile( + @RequestHeader("Authorization") bearerToken: String, + @RequestHeader("Accept") accept: String = "application/vnd.github+json", + @RequestHeader("X-GitHub-Api-Version") apiVersion: String = "2022-11-28", + @PathVariable("owner") owner: String, + @PathVariable("repo") repo: String, + @PathVariable("path") path: String, + @RequestBody request: GithubUpsertFileRequest, + ): GithubUpsertFileResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt new file mode 100644 index 0000000..99d947f --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt @@ -0,0 +1,7 @@ +package com.ixfp.gitmon.client.github.request + +data class GithubUpsertFileRequest( + val message: String, + val content: String, + val sha: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt new file mode 100644 index 0000000..8e33bf3 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.client.github.response + +data class GithubUpsertFileResponse( + val sha: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt new file mode 100644 index 0000000..7273923 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt @@ -0,0 +1,10 @@ +package com.ixfp.gitmon.common.util + +import org.springframework.web.multipart.MultipartFile +import java.util.Base64 + +object Base64Encoder { + fun encodeBase64(file: MultipartFile): String { + return Base64.getUrlEncoder().encodeToString(file.bytes) + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index bdcf4bc..79d03fb 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,6 +1,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER +import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.auth.AuthService @@ -16,10 +17,14 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile import javax.naming.AuthenticationException @RequestMapping("/api/v1/member") @@ -28,6 +33,7 @@ import javax.naming.AuthenticationException class MemberController( private val authService: AuthService, private val memberService: MemberService, + private val githubApiService: GithubApiService, ) { @Operation( summary = "레포지토리 생성/갱신", @@ -121,6 +127,42 @@ class MemberController( } } + @Operation( + summary = "레포지토리 파일 추가", + description = "특정 파일의 base64 인코딩된 내용을 기반으로 파일을 repo에 추가합니다.", + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "파일 업로드 성공"), + ApiResponse(responseCode = "400", description = "잘못된 요청"), + ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + @PutMapping("/repo/upsert_file") + fun upsertFile( + @RequestPart file: MultipartFile, + @RequestParam repo: String, + @RequestParam path: String, + @RequestHeader("Authorization") authorizationHeader: String, + ): ResponseEntity { + return try { + val accessToken = authorizationHeader.removePrefix("Bearer ") + // TODO(KHJ): 여기 에러 핸들링할 것 + githubApiService.upsertFile(accessToken, file, repo, path) + ResponseEntity.ok().build() + } catch (e: Exception) { + log.error(e) { "Failed to upsert file: ${e.message}" } + val status = + when (e) { + is IllegalArgumentException -> HttpStatus.BAD_REQUEST + is AuthenticationException -> HttpStatus.UNAUTHORIZED + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + ResponseEntity.status(status).build() + } + } + @Operation(summary = "레포지토리 URL 조회") @ApiResponses( value = [ From 27f766b39b136ddcbc1b05e77a22f3763567f382 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 17:48:52 +0900 Subject: [PATCH 07/70] =?UTF-8?q?[issue#29]=20=ED=8F=AC=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/client/github/GithubApiService.kt | 12 +----- .../client/github/GithubResourceApiClient.kt | 3 ++ .../com/ixfp/gitmon/config/WebMvcConfig.kt | 1 + .../gitmon/controller/MemberController.kt | 42 +------------------ .../gitmon/controller/PostingController.kt | 18 +++++++- .../request/CreatePostingRequest.kt | 1 - .../gitmon/domain/posting/PostingService.kt | 22 +++++++--- 7 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index fdf1473..f554ee3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -1,7 +1,6 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest -import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubUserResponse import com.ixfp.gitmon.common.util.Base64Encoder @@ -34,6 +33,7 @@ class GithubApiService( fun upsertFile( githubAccessToken: String, content: MultipartFile, + githubUsername: String, repo: String, path: String, commitMessage: String = "Upsert File by API", @@ -44,11 +44,10 @@ class GithubApiService( content = Base64Encoder.encodeBase64(content), sha = "", ) - val owner = "traceoflight" // TODO(KHJ): db에서 owner name 가져오는 부분 대응할 것 val response = githubResourceApiClient.upsertFile( bearerToken = BearerToken(githubAccessToken).format(), - owner = owner, + owner = githubUsername, repo = repo, path = path, request = request, @@ -56,11 +55,4 @@ class GithubApiService( return response.sha } - - private fun createRepository( - token: String, - request: GithubCreateRepositoryRequest, - ) { - githubResourceApiClient.createRepository(token, request) - } } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt index 63d532b..d64eea7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt @@ -51,6 +51,9 @@ interface GithubResourceApiClient { @RequestHeader("X-GitHub-Api-Version") apiVersion: String = "2022-11-28", ): GithubCreateRepositoryResponse + /** + * https://docs.github.com/ko/rest/repos/contents + */ @PutMapping( "/repos/{owner}/{repo}/contents/{path}", consumes = [MediaType.APPLICATION_JSON_VALUE], diff --git a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt index 7ccde3d..549b3c3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt @@ -12,6 +12,7 @@ class WebMvcConfig( override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 + .addPathPatterns("/api/v1/posting/**") } override fun addCorsMappings(registry: CorsRegistry) { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 79d03fb..3c69ab6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,7 +1,7 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.client.github.GithubApiService +import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.auth.AuthService @@ -17,14 +17,10 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute -import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController -import org.springframework.web.multipart.MultipartFile import javax.naming.AuthenticationException @RequestMapping("/api/v1/member") @@ -127,42 +123,6 @@ class MemberController( } } - @Operation( - summary = "레포지토리 파일 추가", - description = "특정 파일의 base64 인코딩된 내용을 기반으로 파일을 repo에 추가합니다.", - ) - @ApiResponses( - value = [ - ApiResponse(responseCode = "200", description = "파일 업로드 성공"), - ApiResponse(responseCode = "400", description = "잘못된 요청"), - ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), - ApiResponse(responseCode = "500", description = "서버 내부 오류"), - ], - ) - @PutMapping("/repo/upsert_file") - fun upsertFile( - @RequestPart file: MultipartFile, - @RequestParam repo: String, - @RequestParam path: String, - @RequestHeader("Authorization") authorizationHeader: String, - ): ResponseEntity { - return try { - val accessToken = authorizationHeader.removePrefix("Bearer ") - // TODO(KHJ): 여기 에러 핸들링할 것 - githubApiService.upsertFile(accessToken, file, repo, path) - ResponseEntity.ok().build() - } catch (e: Exception) { - log.error(e) { "Failed to upsert file: ${e.message}" } - val status = - when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - is AuthenticationException -> HttpStatus.UNAUTHORIZED - else -> HttpStatus.INTERNAL_SERVER_ERROR - } - ResponseEntity.status(status).build() - } - } - @Operation(summary = "레포지토리 URL 조회") @ApiResponses( value = [ diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index cb23462..2bf9221 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -4,29 +4,43 @@ import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreatePostingRequest import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RequestMapping("/api/v1/posting") @RestController class PostingController( private val postingService: PostingService, ) { + @Operation( + summary = "포스팅 생성", + description = "포스팅을 생성합니다.", + security = [SecurityRequirement(name = "accessToken")], + ) @PostMapping fun createPosting( @RequestBody request: CreatePostingRequest, + @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { try { - postingService.create(member, request.title, request.content) + postingService.create(member, request.title, content) return ResponseEntity(HttpStatus.CREATED) } catch (e: Exception) { - return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + val status = when (e) { + is IllegalArgumentException -> HttpStatus.BAD_REQUEST + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + return ResponseEntity(status) } } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt b/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt index a286f86..58bd200 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt @@ -2,5 +2,4 @@ package com.ixfp.gitmon.controller.request data class CreatePostingRequest( val title: String, - val content: String, ) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 841f70c..5507e34 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -1,22 +1,34 @@ package com.ixfp.gitmon.domain.posting +import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberReader import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile @Service class PostingService( private val memberReader: MemberReader, + private val githubApiService: GithubApiService, ) { fun create( member: Member, title: String, - content: String, + content: MultipartFile, ) { - val githubAccessToken = - memberReader.findAccessTokenByMemberId(member.id) - ?: throw Error("Github 엑세스 토큰 없음") + val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) + ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") - // Todo: githubService 연동하여 posting upsert API 호출 + if (member.repoName == null) { + throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") + } + + githubApiService.upsertFile( + githubAccessToken = githubAccessToken, + content = content, + githubUsername = member.githubUsername, + repo = member.repoName, + path = "$title.md", + ) } } From 181f2786d7ec8ed2fe495185ef0bd8a1c56e5130 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 18:03:53 +0900 Subject: [PATCH 08/70] =?UTF-8?q?[issue#29]=20multipart=20form=20data=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/controller/PostingController.kt | 10 ++++++---- .../gitmon/controller/request/CreatePostingRequest.kt | 5 ----- 2 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 2bf9221..8905bc3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -10,8 +10,8 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute -import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile @@ -26,14 +26,16 @@ class PostingController( description = "포스팅을 생성합니다.", security = [SecurityRequirement(name = "accessToken")], ) - @PostMapping + @PostMapping( + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + ) fun createPosting( - @RequestBody request: CreatePostingRequest, + @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { try { - postingService.create(member, request.title, content) + postingService.create(member, title, content) return ResponseEntity(HttpStatus.CREATED) } catch (e: Exception) { val status = when (e) { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt b/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt deleted file mode 100644 index 58bd200..0000000 --- a/src/main/kotlin/com/ixfp/gitmon/controller/request/CreatePostingRequest.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ixfp.gitmon.controller.request - -data class CreatePostingRequest( - val title: String, -) From c9060eeb50ee76bb44719d292249e09e9d163df3 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 18:05:30 +0900 Subject: [PATCH 09/70] =?UTF-8?q?config=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../config => config/docs}/SpringDocConfig.kt | 15 ++++++++-- .../docs/SwaggerSecurityRequirements.kt | 3 ++ .../config/{ => web}/JwtAuthInterceptor.kt | 29 +++++++++---------- .../web}/RequestHeaderAttributes.kt | 2 +- .../gitmon/config/{ => web}/WebMvcConfig.kt | 2 +- 5 files changed, 30 insertions(+), 21 deletions(-) rename src/main/kotlin/com/ixfp/gitmon/{common/config => config/docs}/SpringDocConfig.kt (84%) create mode 100644 src/main/kotlin/com/ixfp/gitmon/config/docs/SwaggerSecurityRequirements.kt rename src/main/kotlin/com/ixfp/gitmon/config/{ => web}/JwtAuthInterceptor.kt (61%) rename src/main/kotlin/com/ixfp/gitmon/{common/const => config/web}/RequestHeaderAttributes.kt (60%) rename src/main/kotlin/com/ixfp/gitmon/config/{ => web}/WebMvcConfig.kt (97%) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/config/SpringDocConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt similarity index 84% rename from src/main/kotlin/com/ixfp/gitmon/common/config/SpringDocConfig.kt rename to src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt index a8c6c91..a9231e3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/config/SpringDocConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt @@ -1,4 +1,4 @@ -package com.ixfp.gitmon.common.config +package com.ixfp.gitmon.config.docs import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI @@ -9,6 +9,7 @@ import io.swagger.v3.oas.models.media.MediaType import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.responses.ApiResponse import io.swagger.v3.oas.models.responses.ApiResponses +import io.swagger.v3.oas.models.security.SecurityScheme import org.springdoc.core.customizers.OpenApiCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -19,13 +20,21 @@ data class ResponseMsg( val timestamp: String = "2025-01-01T12:00:00Z", ) -// TODO(KHJ): 인증 관련 처리 필요함 @Configuration class SpringDocConfig { // SpringDoc Main Title 세팅 @Bean fun customOpenAPI(): OpenAPI { - return OpenAPI().info(configurationInfo()).components(Components()) + return OpenAPI().info(configurationInfo()).components( + Components().addSecuritySchemes( + ACCESS_TOKEN, + SecurityScheme() + .name(ACCESS_TOKEN) + .type(SecurityScheme.Type.APIKEY) + .`in`(SecurityScheme.In.HEADER) + .bearerFormat("JWT"), + ), + ) } private fun configurationInfo(): Info { diff --git a/src/main/kotlin/com/ixfp/gitmon/config/docs/SwaggerSecurityRequirements.kt b/src/main/kotlin/com/ixfp/gitmon/config/docs/SwaggerSecurityRequirements.kt new file mode 100644 index 0000000..bf09f7d --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/config/docs/SwaggerSecurityRequirements.kt @@ -0,0 +1,3 @@ +package com.ixfp.gitmon.config.docs + +const val ACCESS_TOKEN = "accessToken" diff --git a/src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt similarity index 61% rename from src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt rename to src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt index 119e316..367b8ca 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/JwtAuthInterceptor.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt @@ -1,6 +1,5 @@ -package com.ixfp.gitmon.config +package com.ixfp.gitmon.config.web -import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER import com.ixfp.gitmon.common.util.JwtUtil import com.ixfp.gitmon.domain.auth.AuthService import io.github.oshai.kotlinlogging.KotlinLogging.logger @@ -27,21 +26,19 @@ class JwtAuthInterceptor( } val token = authHeader.removePrefix("Bearer ") - val exposedId = - jwtUtil.parseAccessToken(token)?.exposedId - ?: run { - log.info { "유효하지 않은 사용자 토큰" } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") - return false - } + val exposedId = jwtUtil.parseAccessToken(token)?.exposedId + ?: run { + log.info { "유효하지 않은 사용자 토큰" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } - val member = - authService.getMemberByExposedId(exposedId) - ?: run { - log.info { "토큰에 해당하는 사용자를 찾을 수 없음" } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") - return false - } + val member = authService.getMemberByExposedId(exposedId) + ?: run { + log.info { "토큰에 해당하는 사용자를 찾을 수 없음" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } request.setAttribute(AUTHENTICATED_MEMBER, member) // 요청 속성에 저장하여 컨트롤러에서 사용 가능 return true diff --git a/src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/RequestHeaderAttributes.kt similarity index 60% rename from src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt rename to src/main/kotlin/com/ixfp/gitmon/config/web/RequestHeaderAttributes.kt index 3948b74..100be18 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/const/RequestHeaderAttributes.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/RequestHeaderAttributes.kt @@ -1,3 +1,3 @@ -package com.ixfp.gitmon.common.const +package com.ixfp.gitmon.config.web const val AUTHENTICATED_MEMBER = "authenticatedMember" diff --git a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt similarity index 97% rename from src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt rename to src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index 549b3c3..19133aa 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -1,4 +1,4 @@ -package com.ixfp.gitmon.config +package com.ixfp.gitmon.config.web import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.CorsRegistry From e915467770476328f79231163a9b324eed3a02ae Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 18:05:47 +0900 Subject: [PATCH 10/70] =?UTF-8?q?ktlint=20=EC=84=A4=EC=A0=95=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .editorconfig | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..283921e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*.{kt,kts}] +ktlint_code_style = intellij_idea \ No newline at end of file From 09a294e0a95793b3475aa5c0d640dfaa4ecce345 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 18:08:56 +0900 Subject: [PATCH 11/70] =?UTF-8?q?=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20swagger=20ui=20SecurityRequirement=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../kotlin/com/ixfp/gitmon/controller/MemberController.kt | 8 +++++--- .../com/ixfp/gitmon/controller/PostingController.kt | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 3c69ab6..ef4720c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,7 +1,7 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.client.github.GithubApiService -import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER +import com.ixfp.gitmon.config.docs.ACCESS_TOKEN +import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.auth.AuthService @@ -11,6 +11,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -29,11 +30,11 @@ import javax.naming.AuthenticationException class MemberController( private val authService: AuthService, private val memberService: MemberService, - private val githubApiService: GithubApiService, ) { @Operation( summary = "레포지토리 생성/갱신", description = "이미 설정된 레포지토리가 있다면 갱신하고, 없다면 새로 생성합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], ) @ApiResponses( value = [ @@ -89,6 +90,7 @@ class MemberController( @Operation( summary = "레포지토리 이름 중복 조회", description = "입력한 레포지토리 이름이 이미 사용 중인지 확인합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], ) @ApiResponses( value = [ diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 8905bc3..ccd6f51 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,12 +1,13 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.const.AUTHENTICATED_MEMBER -import com.ixfp.gitmon.controller.request.CreatePostingRequest +import com.ixfp.gitmon.config.docs.ACCESS_TOKEN +import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute @@ -24,7 +25,7 @@ class PostingController( @Operation( summary = "포스팅 생성", description = "포스팅을 생성합니다.", - security = [SecurityRequirement(name = "accessToken")], + security = [SecurityRequirement(name = ACCESS_TOKEN)], ) @PostMapping( consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], From eb8fdbbf45ee01259a0efc6a15801679f2687273 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 18:24:52 +0900 Subject: [PATCH 12/70] fix ktlint Signed-off-by: Suhjeong Kim --- .editorconfig | 4 --- .../gitmon/config/web/JwtAuthInterceptor.kt | 26 ++++++++++--------- .../gitmon/controller/PostingController.kt | 9 ++++--- .../gitmon/domain/posting/PostingService.kt | 5 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 283921e..0000000 --- a/.editorconfig +++ /dev/null @@ -1,4 +0,0 @@ -root = true - -[*.{kt,kts}] -ktlint_code_style = intellij_idea \ No newline at end of file diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt index 367b8ca..8f9f42a 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt @@ -26,19 +26,21 @@ class JwtAuthInterceptor( } val token = authHeader.removePrefix("Bearer ") - val exposedId = jwtUtil.parseAccessToken(token)?.exposedId - ?: run { - log.info { "유효하지 않은 사용자 토큰" } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") - return false - } + val exposedId = + jwtUtil.parseAccessToken(token)?.exposedId + ?: run { + log.info { "유효하지 않은 사용자 토큰" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } - val member = authService.getMemberByExposedId(exposedId) - ?: run { - log.info { "토큰에 해당하는 사용자를 찾을 수 없음" } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") - return false - } + val member = + authService.getMemberByExposedId(exposedId) + ?: run { + log.info { "토큰에 해당하는 사용자를 찾을 수 없음" } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token") + return false + } request.setAttribute(AUTHENTICATED_MEMBER, member) // 요청 속성에 저장하여 컨트롤러에서 사용 가능 return true diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index ccd6f51..d4df43a 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -39,10 +39,11 @@ class PostingController( postingService.create(member, title, content) return ResponseEntity(HttpStatus.CREATED) } catch (e: Exception) { - val status = when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - else -> HttpStatus.INTERNAL_SERVER_ERROR - } + val status = + when (e) { + is IllegalArgumentException -> HttpStatus.BAD_REQUEST + else -> HttpStatus.INTERNAL_SERVER_ERROR + } return ResponseEntity(status) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 5507e34..cbcbb04 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -16,8 +16,9 @@ class PostingService( title: String, content: MultipartFile, ) { - val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) - ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") + val githubAccessToken = + memberReader.findAccessTokenByMemberId(member.id) + ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") if (member.repoName == null) { throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") From 6a88ec5fc690b98e6f9d297bf6bdd081eb5758a2 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 20:08:10 +0900 Subject: [PATCH 13/70] =?UTF-8?q?cors=20origin=20=EC=97=90=20api=20url=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(swagger=20ui=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index 19133aa..d3aadaf 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -24,8 +24,9 @@ class WebMvcConfig( } companion object { - private val GITMON_CLIENT_URL = "https://gitmon.blog" - private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL) + private const val GITMON_CLIENT_URL = "https://gitmon.blog" + private const val GITMON_API_URL = "https://api.gitmon.blog" + private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL, GITMON_API_URL) private val ALLOWED_METHODS = arrayOf("GET", "POST", "PUT", "DELETE", "OPTIONS") } } From ce016ddb9c3c376e2fdbe0ad0f5c18afc6768b9a Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 20:40:17 +0900 Subject: [PATCH 14/70] =?UTF-8?q?swagger=20failed=20to=20fetch=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/config/docs/SpringDocConfig.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt index a9231e3..1cddd4c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt @@ -10,6 +10,7 @@ import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.responses.ApiResponse import io.swagger.v3.oas.models.responses.ApiResponses import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server import org.springdoc.core.customizers.OpenApiCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -25,16 +26,19 @@ class SpringDocConfig { // SpringDoc Main Title 세팅 @Bean fun customOpenAPI(): OpenAPI { - return OpenAPI().info(configurationInfo()).components( - Components().addSecuritySchemes( - ACCESS_TOKEN, - SecurityScheme() - .name(ACCESS_TOKEN) - .type(SecurityScheme.Type.APIKEY) - .`in`(SecurityScheme.In.HEADER) - .bearerFormat("JWT"), - ), - ) + return OpenAPI() + .info(configurationInfo()) + .components( + Components().addSecuritySchemes( + ACCESS_TOKEN, + SecurityScheme() + .name(ACCESS_TOKEN) + .type(SecurityScheme.Type.APIKEY) + .`in`(SecurityScheme.In.HEADER) + .bearerFormat("JWT"), + ), + ) + .addServersItem(Server().url("https://api.gitmon.blog")) } private fun configurationInfo(): Info { From f63ed9927c4c59a741d331960a3453d915772911 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 20:52:23 +0900 Subject: [PATCH 15/70] =?UTF-8?q?swagger=20ui=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EC=84=A4=EC=A0=95=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt index 1cddd4c..8fba1f8 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt @@ -33,8 +33,9 @@ class SpringDocConfig { ACCESS_TOKEN, SecurityScheme() .name(ACCESS_TOKEN) - .type(SecurityScheme.Type.APIKEY) + .type(SecurityScheme.Type.HTTP) .`in`(SecurityScheme.In.HEADER) + .scheme("bearer") .bearerFormat("JWT"), ), ) From 228b777e69b8ea6db51649525e893ec86a6157aa Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 21:10:15 +0900 Subject: [PATCH 16/70] =?UTF-8?q?posting=20controller=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../kotlin/com/ixfp/gitmon/controller/PostingController.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index d4df43a..e68580c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -4,6 +4,7 @@ import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService +import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.security.SecurityRequirement import org.springframework.http.HttpStatus @@ -44,7 +45,12 @@ class PostingController( is IllegalArgumentException -> HttpStatus.BAD_REQUEST else -> HttpStatus.INTERNAL_SERVER_ERROR } + log.error { "포스팅 생성 중 에러 발생: ${e.message}" } return ResponseEntity(status) } } + + companion object { + private val log = logger {} + } } From 35481523a57c14f52f760f2c75f1a62b531756ea Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 21:36:04 +0900 Subject: [PATCH 17/70] =?UTF-8?q?base64=20standard=20encoder=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt index 7273923..9ffe4d7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt @@ -5,6 +5,6 @@ import java.util.Base64 object Base64Encoder { fun encodeBase64(file: MultipartFile): String { - return Base64.getUrlEncoder().encodeToString(file.bytes) + return Base64.getEncoder().encodeToString(file.bytes) } } From 26934d8aef4cd0b4e0bfd4be116873bd4f5a164d Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 23 Mar 2025 21:57:14 +0900 Subject: [PATCH 18/70] =?UTF-8?q?GithubUpsertFileResponse=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/client/github/GithubApiService.kt | 2 +- .../response/GithubUpsertFileResponse.kt | 6 +++++ .../gitmon/domain/posting/PostingService.kt | 22 +++++++++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index f554ee3..c4a1e8e 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -53,6 +53,6 @@ class GithubApiService( request = request, ) - return response.sha + return response.content.sha } } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt index 8e33bf3..cbab07f 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt @@ -1,5 +1,11 @@ package com.ixfp.gitmon.client.github.response data class GithubUpsertFileResponse( + val content: GithubContent, +) + +data class GithubContent( + val name: String, + val path: String, val sha: String, ) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index cbcbb04..95b20a9 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -3,6 +3,7 @@ package com.ixfp.gitmon.domain.posting import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberReader +import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile @@ -16,6 +17,7 @@ class PostingService( title: String, content: MultipartFile, ) { + log.info { "PostingService#create start. member=$member, title=$title, content.size=${content.size}" } val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") @@ -24,12 +26,18 @@ class PostingService( throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") } - githubApiService.upsertFile( - githubAccessToken = githubAccessToken, - content = content, - githubUsername = member.githubUsername, - repo = member.repoName, - path = "$title.md", - ) + val contentSha = + githubApiService.upsertFile( + githubAccessToken = githubAccessToken, + content = content, + githubUsername = member.githubUsername, + repo = member.repoName, + path = "$title.md", + ) + log.info { "PostingService#create end. contentSha=$contentSha" } + } + + companion object { + private val log = logger {} } } From 1d19468a738d643eb3e76716a810a4905a0772a1 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 29 Mar 2025 10:27:31 +0900 Subject: [PATCH 19/70] =?UTF-8?q?CORS=20localhost:3000=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index d3aadaf..d623b0c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -26,7 +26,7 @@ class WebMvcConfig( companion object { private const val GITMON_CLIENT_URL = "https://gitmon.blog" private const val GITMON_API_URL = "https://api.gitmon.blog" - private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL, GITMON_API_URL) + private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL, GITMON_API_URL, "http://localhost:3000") private val ALLOWED_METHODS = arrayOf("GET", "POST", "PUT", "DELETE", "OPTIONS") } } From 36e3683ef62d10eacb803e602bc1dbe23d26b2cb Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 29 Mar 2025 10:31:13 +0900 Subject: [PATCH 20/70] =?UTF-8?q?CORS=20http://127.0.0.1:3000=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index d623b0c..90f55fd 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -26,7 +26,7 @@ class WebMvcConfig( companion object { private const val GITMON_CLIENT_URL = "https://gitmon.blog" private const val GITMON_API_URL = "https://api.gitmon.blog" - private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL, GITMON_API_URL, "http://localhost:3000") + private val ALLOWED_ORIGINS = arrayOf(GITMON_CLIENT_URL, GITMON_API_URL, "http://localhost:3000", "http://127.0.0.1:3000") private val ALLOWED_METHODS = arrayOf("GET", "POST", "PUT", "DELETE", "OPTIONS") } } From 1aa2651b9fe20e7adf698bd5178772e960e0cf2b Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 29 Mar 2025 10:56:23 +0900 Subject: [PATCH 21/70] =?UTF-8?q?gitmon=20accessToken=20=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20API=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20id=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../ixfp/gitmon/controller/Oauth2Controller.kt | 17 ++++++++++++----- .../controller/response/AccessTokenResponse.kt | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index e125fb7..3786280 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -91,14 +91,21 @@ class Oauth2Controller( try { val githubAccessToken = githubApiService.getAccessTokenByCode(request.code) val githubUser = githubApiService.getGithubUser(githubAccessToken) - var member = authService.getMemberByGithubId(githubUser.id.toLong()) - if (member == null) { - member = authService.signup(githubAccessToken, githubUser) - } + val member = + authService.getMemberByGithubId(githubUser.id.toLong()) + ?: authService.signup(githubAccessToken, githubUser) + authService.login(githubAccessToken, member) + val accessToken = authService.createAccessToken(member) val isRepoCreated = member.repoName != null - val response = AccessTokenResponse(accessToken, isRepoCreated) + + val response = + AccessTokenResponse( + id = member.exposedId, + accessToken = accessToken, + isRepoCreated = isRepoCreated, + ) return ResponseEntity.status(HttpStatus.CREATED).body(response) } catch (e: Exception) { log.error(e) { "Failed to login: ${e.message}" } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt index a72d697..9151f58 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt @@ -1,6 +1,7 @@ package com.ixfp.gitmon.controller.response data class AccessTokenResponse( + val id: String, val accessToken: String, val isRepoCreated: Boolean, ) From e6ea1936fc3490130371d1247dd6720e5a71f1fd Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 29 Mar 2025 11:03:37 +0900 Subject: [PATCH 22/70] =?UTF-8?q?JwtAuthInterceptor=20=EC=97=90=EC=84=9C?= =?UTF-8?q?=20OPTIONS=20=ED=86=B5=EA=B3=BC=20=EC=B2=98=EB=A6=AC=20(preflig?= =?UTF-8?q?ht)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt index 8f9f42a..8dec900 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/JwtAuthInterceptor.kt @@ -18,6 +18,10 @@ class JwtAuthInterceptor( response: HttpServletResponse, handler: Any, ): Boolean { + if (request.method.equals("OPTIONS", ignoreCase = true)) { + return true + } + val authHeader = request.getHeader("Authorization") if (authHeader.isNullOrBlank() || !authHeader.startsWith("Bearer ")) { From a45d817690f0276a03ac4d0a5911d6072a3fd06e Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Wed, 2 Apr 2025 22:10:19 +0900 Subject: [PATCH 23/70] =?UTF-8?q?GithubApiService=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/client/github/GithubApiService.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index c4a1e8e..d88faaf 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -5,6 +5,7 @@ import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubUserResponse import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken +import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile @@ -21,13 +22,16 @@ class GithubApiService( } fun getAccessTokenByCode(code: String): String { + log.info { "GithubApiService#getAccessTokenByCode start. code=$code" } val request = GithubAccessTokenRequest( code = code, client_id = githubClientId, client_secret = githubClientSecret, ) - return githubOauth2ApiClient.fetchAccessToken(request).accessToken + val response = githubOauth2ApiClient.fetchAccessToken(request) + log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } + return response.accessToken } fun upsertFile( @@ -55,4 +59,8 @@ class GithubApiService( return response.content.sha } + + companion object { + private val log = logger {} + } } From 6bcced85c103e705e6957020f45138e3e029e71a Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Wed, 2 Apr 2025 22:18:16 +0900 Subject: [PATCH 24/70] =?UTF-8?q?github=20access=20token=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/client/github/GithubApiService.kt | 3 +++ .../github/response/GithubAccessTokenResponse.kt | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index d88faaf..825d822 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -31,6 +31,9 @@ class GithubApiService( ) val response = githubOauth2ApiClient.fetchAccessToken(request) log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } + if (response.accessToken == null) { + throw RuntimeException("Failed to get access token from github. response=$response") + } return response.accessToken } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubAccessTokenResponse.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubAccessTokenResponse.kt index 6c477e5..068e347 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubAccessTokenResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubAccessTokenResponse.kt @@ -4,8 +4,13 @@ import com.fasterxml.jackson.annotation.JsonProperty data class GithubAccessTokenResponse( @JsonProperty("access_token") - val accessToken: String, - val scope: String, + val accessToken: String?, + val scope: String?, @JsonProperty("token_type") - val tokenType: String, + val tokenType: String?, + val error: String?, + @JsonProperty("error_description") + val errorDescription: String?, + @JsonProperty("error_uri") + val errorUri: String?, ) From 8925cc41870401f528780b94b9a6a2ba64d75edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Fri, 4 Apr 2025 14:17:39 +0900 Subject: [PATCH 25/70] =?UTF-8?q?feat:=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 깃허브 file upsert 응답에 download_url 추가 - 깃허브 이미지 업로드 메서드 구현 --- .../gitmon/client/github/GithubApiService.kt | 5 +- .../response/GithubUpsertFileResponse.kt | 1 + .../gitmon/domain/posting/PostingService.kt | 49 ++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 825d822..650aa76 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -2,6 +2,7 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest +import com.ixfp.gitmon.client.github.response.GithubContent import com.ixfp.gitmon.client.github.response.GithubUserResponse import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken @@ -44,7 +45,7 @@ class GithubApiService( repo: String, path: String, commitMessage: String = "Upsert File by API", - ): String { + ): GithubContent { val request = GithubUpsertFileRequest( message = "Add New File", @@ -60,7 +61,7 @@ class GithubApiService( request = request, ) - return response.content.sha + return response.content } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt index cbab07f..4381209 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt @@ -8,4 +8,5 @@ data class GithubContent( val name: String, val path: String, val sha: String, + val download_url: String, ) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 95b20a9..e19efb2 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -6,6 +6,7 @@ import com.ixfp.gitmon.domain.member.MemberReader import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile +import java.util.UUID @Service class PostingService( @@ -26,7 +27,7 @@ class PostingService( throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") } - val contentSha = + val githubContent = githubApiService.upsertFile( githubAccessToken = githubAccessToken, content = content, @@ -34,7 +35,51 @@ class PostingService( repo = member.repoName, path = "$title.md", ) - log.info { "PostingService#create end. contentSha=$contentSha" } + log.info { "PostingService#create end. contentSha=$githubContent.sha" } + } + + private fun uploadImage( + member: Member, + content: MultipartFile, + ): String { + val githubAccessToken = + memberReader.findAccessTokenByMemberId(member.id) + ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") + + if (member.repoName == null) { + throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") + } + + val extension = resolveExtension(content) ?: throw IllegalArgumentException("파일의 확장자가 이미지가 아닙니다.") + val imageId = UUID.randomUUID().toString() + val filename = "$imageId.$extension" + val path = "images/$filename" + + val githubContent = + githubApiService.upsertFile( + githubAccessToken = githubAccessToken, + content = content, + githubUsername = member.githubUsername, + repo = member.repoName, + path = path, + ) + + return githubContent.download_url + } + + private fun resolveExtension(file: MultipartFile): String? { + val contentType = file.contentType + if (contentType != null && contentType.startsWith("image/")) { + val subtype = contentType.substringAfter("image/") + if (subtype.isNotBlank()) { + return subtype + } + } + val extension = file.originalFilename?.substringAfterLast('.', "") + if (!extension.isNullOrBlank()) { + return extension + } + return null } companion object { From 9991a8eec04c64826d4dfb2972ff29f1ac979f7d Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 12 Apr 2025 21:04:37 +0900 Subject: [PATCH 26/70] =?UTF-8?q?[issue#43]=20Swagger=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20Controller=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/controller/IMemberController.kt | 77 +++++++++++++++++++ .../gitmon/controller/IOauth2Controller.kt | 65 ++++++++++++++++ .../gitmon/controller/IPostingController.kt | 24 ++++++ .../gitmon/controller/MemberController.kt | 66 ++-------------- .../gitmon/controller/Oauth2Controller.kt | 60 +-------------- .../gitmon/controller/PostingController.kt | 14 +--- 6 files changed, 177 insertions(+), 129 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt new file mode 100644 index 0000000..fc64699 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -0,0 +1,77 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.config.docs.ACCESS_TOKEN +import com.ixfp.gitmon.controller.request.CreateRepoRequest +import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse +import com.ixfp.gitmon.domain.member.Member +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +@Tag(name = "Member", description = "회원 관련 API") +interface IMemberController { + @Operation( + summary = "레포지토리 생성/갱신", + description = "이미 설정된 레포지토리가 있다면 갱신하고, 없다면 새로 생성합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "201", + description = "레포 생성/갱신 성공", + ), + ApiResponse( + responseCode = "200", + description = "레포지토리 이름이 설정된 사용자 레포 이름과 동일함", + ), + ApiResponse( + responseCode = "400", + description = "잘못된 요청 (예: 파라미터 오류 등)", + ), + ApiResponse( + responseCode = "401", + description = "인증 실패 (엑세스 토큰 문제)", + ), + ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + ), + ], + ) + fun upsertRepo( + request: CreateRepoRequest, + member: Member, + ): ResponseEntity + + @Operation( + summary = "레포지토리 이름 중복 조회", + description = "입력한 레포지토리 이름이 이미 사용 중인지 확인합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "사용 가능한 레포지토리 이름"), + ApiResponse(responseCode = "409", description = "이미 사용 중인 레포지토리 이름"), + ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), + ApiResponse(responseCode = "400", description = "잘못된 요청"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun checkRepoName( + name: String, + member: Member, + ): ResponseEntity + + @Operation(summary = "레포지토리 URL 조회") + @ApiResponses( + value = [ + ApiResponse(responseCode = "404", description = "레포지토리 URL 찾을 수 없음"), + ], + ) + fun findGithubRepoUrl(githubUsername: String): ResponseEntity +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt new file mode 100644 index 0000000..329bb34 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -0,0 +1,65 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.controller.request.GithubOauth2Request +import com.ixfp.gitmon.controller.response.AccessTokenResponse +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.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +@Tag(name = "OAuth Token Control", description = "OAuth 인증 관련 API") +interface IOauth2Controller { + @Operation(summary = "Get Oauth2 Token", description = "Github으로부터 Oauth2 Token을 받아오는 API") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "301", + description = "Success", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = ArraySchema(schema = Schema(implementation = String::class)), + ), + ], + ), + ], + ) + fun redirectToGithubOauthUrl(): ResponseEntity + + @Operation( + summary = "gitmon Access 토큰 발급", + description = "Github OAuth 인증 코드를 받아 로그인 처리 후 gitmon의 Access Token을 발급 받음.", + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "201", + description = "로그인 및 토큰 발급 성공", + content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = AccessTokenResponse::class), + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "잘못된 요청 (code가 유효하지 않은 경우 등)", + ), + ApiResponse( + responseCode = "401", + description = "인증 실패", + ), + ApiResponse( + responseCode = "500", + description = "서버 에러", + ), + ], + ) + fun login(request: GithubOauth2Request): ResponseEntity +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt new file mode 100644 index 0000000..515fe46 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -0,0 +1,24 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.config.docs.ACCESS_TOKEN +import com.ixfp.gitmon.domain.member.Member +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.multipart.MultipartFile + +@Tag(name = "Posting", description = "게시글(포스팅) 관련 API") +interface IPostingController { + @Operation( + summary = "포스팅 생성", + description = "포스팅을 생성합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], + ) + fun createPosting( + title: String, + content: MultipartFile, + member: Member, + ): ResponseEntity +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index ef4720c..e7305c0 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,6 +1,5 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse @@ -8,11 +7,6 @@ import com.ixfp.gitmon.domain.auth.AuthService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.responses.ApiResponse -import io.swagger.v3.oas.annotations.responses.ApiResponses -import io.swagger.v3.oas.annotations.security.SecurityRequirement -import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -24,44 +18,14 @@ import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import javax.naming.AuthenticationException +@RestController @RequestMapping("/api/v1/member") -@RestController() -@Tag(name = "Member", description = "회원 관련 API") class MemberController( private val authService: AuthService, private val memberService: MemberService, -) { - @Operation( - summary = "레포지토리 생성/갱신", - description = "이미 설정된 레포지토리가 있다면 갱신하고, 없다면 새로 생성합니다.", - security = [SecurityRequirement(name = ACCESS_TOKEN)], - ) - @ApiResponses( - value = [ - ApiResponse( - responseCode = "201", - description = "레포 생성/갱신 성공", - ), - ApiResponse( - responseCode = "200", - description = "레포지토리 이름이 설정된 사용자 레포 이름과 동일함", - ), - ApiResponse( - responseCode = "400", - description = "잘못된 요청 (예: 파라미터 오류 등)", - ), - ApiResponse( - responseCode = "401", - description = "인증 실패 (엑세스 토큰 문제)", - ), - ApiResponse( - responseCode = "500", - description = "서버 내부 오류", - ), - ], - ) +) : IMemberController { @PostMapping("/repo") - fun upsertRepo( + override fun upsertRepo( @RequestBody request: CreateRepoRequest, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { @@ -87,22 +51,8 @@ class MemberController( } } - @Operation( - summary = "레포지토리 이름 중복 조회", - description = "입력한 레포지토리 이름이 이미 사용 중인지 확인합니다.", - security = [SecurityRequirement(name = ACCESS_TOKEN)], - ) - @ApiResponses( - value = [ - ApiResponse(responseCode = "200", description = "사용 가능한 레포지토리 이름"), - ApiResponse(responseCode = "409", description = "이미 사용 중인 레포지토리 이름"), - ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), - ApiResponse(responseCode = "400", description = "잘못된 요청"), - ApiResponse(responseCode = "500", description = "서버 내부 오류"), - ], - ) @GetMapping("/repo/check") - fun checkRepoName( + override fun checkRepoName( @RequestParam name: String, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity { @@ -125,14 +75,8 @@ class MemberController( } } - @Operation(summary = "레포지토리 URL 조회") - @ApiResponses( - value = [ - ApiResponse(responseCode = "404", description = "레포지토리 URL 찾을 수 없음"), - ], - ) @GetMapping("/github/repo") - fun findGithubRepoUrl( + override fun findGithubRepoUrl( @RequestParam githubUsername: String, ): ResponseEntity { val githubRepoUrl = diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 3786280..c376711 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -5,16 +5,8 @@ import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse import com.ixfp.gitmon.domain.auth.AuthService import io.github.oshai.kotlinlogging.KotlinLogging.logger -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.responses.ApiResponses -import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -25,67 +17,21 @@ import javax.naming.AuthenticationException @RestController @RequestMapping("/api/v1") -@Tag(name = "OAuth Token Control", description = "OAuth 인증 관련 API") class Oauth2Controller( @Value("\${oauth2.client.github.id}") private val githubClientId: String, private val authService: AuthService, private val githubApiService: GithubApiService, -) { - @Operation(summary = "Get Oauth2 Token", description = "Github으로부터 Oauth2 Token을 받아오는 API") - @ApiResponses( - value = [ - ApiResponse( - responseCode = "301", - description = "Success", - content = [ - Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - array = ArraySchema(schema = Schema(implementation = String::class)), - ), - ], - ), - ], - ) +) : IOauth2Controller { @GetMapping("/login/oauth/github") - fun redirectToGithubOauthUrl(): ResponseEntity { + override fun redirectToGithubOauthUrl(): ResponseEntity { return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header( "Location", "https://github.com/login/oauth/authorize?&scope=repo&client_id=$githubClientId", ).build() } - @Operation( - summary = "gitmon Access 토큰 발급", - description = "Github OAuth 인증 코드를 받아 로그인 처리 후 gitmon의 Access Token을 발급 받음.", - ) - @ApiResponses( - value = [ - ApiResponse( - responseCode = "201", - description = "로그인 및 토큰 발급 성공", - content = [ - Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = Schema(implementation = AccessTokenResponse::class), - ), - ], - ), - ApiResponse( - responseCode = "400", - description = "잘못된 요청 (code가 유효하지 않은 경우 등)", - ), - ApiResponse( - responseCode = "401", - description = "인증 실패", - ), - ApiResponse( - responseCode = "500", - description = "서버 에러", - ), - ], - ) @PostMapping("/login/oauth/github/tokens") - fun login( + override fun login( @RequestBody request: GithubOauth2Request, ): ResponseEntity { try { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index e68580c..4f72899 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,12 +1,9 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.security.SecurityRequirement import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -18,20 +15,15 @@ import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile -@RequestMapping("/api/v1/posting") @RestController +@RequestMapping("/api/v1/posting") class PostingController( private val postingService: PostingService, -) { - @Operation( - summary = "포스팅 생성", - description = "포스팅을 생성합니다.", - security = [SecurityRequirement(name = ACCESS_TOKEN)], - ) +) : IPostingController { @PostMapping( consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], ) - fun createPosting( + override fun createPosting( @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, From 1870c69ce8702a76cea59773300362fb0dbc5842 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 12 Apr 2025 23:09:04 +0900 Subject: [PATCH 27/70] =?UTF-8?q?[issue#45]=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=8A=A4=ED=8E=99=20=ED=9A=8D=EC=9D=BC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../ixfp/gitmon/common/type/ApiResponse.kt | 51 +++++++++++ .../gitmon/config/docs/SpringDocConfig.kt | 4 +- .../gitmon/controller/IMemberController.kt | 91 +++++++++++++------ .../gitmon/controller/IOauth2Controller.kt | 31 +++---- .../gitmon/controller/IPostingController.kt | 30 +++++- .../gitmon/controller/MemberController.kt | 45 +++++---- .../gitmon/controller/Oauth2Controller.kt | 16 ++-- .../gitmon/controller/PostingController.kt | 16 ++-- .../response/CheckRepoNameResponse.kt | 5 + 9 files changed, 200 insertions(+), 89 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/response/CheckRepoNameResponse.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt new file mode 100644 index 0000000..6616795 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt @@ -0,0 +1,51 @@ +package com.ixfp.gitmon.common.type + +data class ApiResponse( + val status: ApiStatus, + val data: T? = null, + val error: ApiErrorType? = null, +) { + constructor(data: T) : this( + status = ApiStatus.SUCCESS, + data = data, + ) + + companion object { + fun success(): ApiResponse { + return ApiResponse( + status = ApiStatus.SUCCESS, + data = null, + ) + } + + fun success(data: T): ApiResponse { + return ApiResponse( + status = ApiStatus.SUCCESS, + data = data, + ) + } + + fun error(error: ApiErrorType): ApiResponse { + return ApiResponse( + status = error.status, + error = error, + ) + } + } +} + +enum class ApiStatus { + SUCCESS, + UNAUTHORIZED, + FORBIDDEN, + ERROR, +} + +enum class ApiErrorType(val status: ApiStatus, message: String) { + BAD_REQUEST(ApiStatus.ERROR, "Bad request"), + UNAUTHORIZED(ApiStatus.UNAUTHORIZED, "Unauthorized"), + INVALID_ACCESS_TOKEN(ApiStatus.UNAUTHORIZED, "Invalid access token"), + EXPIRED_ACCESS_TOKEN(ApiStatus.UNAUTHORIZED, "Access token expired"), + + INTERNAL_SERVER_ERROR(ApiStatus.ERROR, "Internal server error"), +} diff --git a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt index 8fba1f8..168c182 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/docs/SpringDocConfig.kt @@ -55,8 +55,8 @@ class SpringDocConfig { val apiResponses: ApiResponses = operation.responses // 미지원 Standard Response 추가 - apiResponses.addApiResponse("400", customApiResponse("400", "Bad Request")) - apiResponses.addApiResponse("500", customApiResponse("500", "Internal Server Error")) +// apiResponses.addApiResponse("400", customApiResponse("400", "Bad Request")) +// apiResponses.addApiResponse("500", customApiResponse("500", "Internal Server Error")) } } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index fc64699..49690a9 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -2,15 +2,16 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.controller.request.CreateRepoRequest +import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.member.Member 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.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity @Tag(name = "Member", description = "회원 관련 API") interface IMemberController { @@ -22,56 +23,88 @@ interface IMemberController { @ApiResponses( value = [ ApiResponse( - responseCode = "201", description = "레포 생성/갱신 성공", - ), - ApiResponse( - responseCode = "200", - description = "레포지토리 이름이 설정된 사용자 레포 이름과 동일함", - ), - ApiResponse( - responseCode = "400", - description = "잘못된 요청 (예: 파라미터 오류 등)", - ), - ApiResponse( - responseCode = "401", - description = "인증 실패 (엑세스 토큰 문제)", - ), - ApiResponse( - responseCode = "500", - description = "서버 내부 오류", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": null + } + """, + ), + ), + ], ), ], ) fun upsertRepo( request: CreateRepoRequest, member: Member, - ): ResponseEntity + ): com.ixfp.gitmon.common.type.ApiResponse @Operation( summary = "레포지토리 이름 중복 조회", - description = "입력한 레포지토리 이름이 이미 사용 중인지 확인합니다.", + description = "입력한 레포지토리 이름을 사용할 수 있는지 확인합니다.", security = [SecurityRequirement(name = ACCESS_TOKEN)], ) @ApiResponses( value = [ - ApiResponse(responseCode = "200", description = "사용 가능한 레포지토리 이름"), - ApiResponse(responseCode = "409", description = "이미 사용 중인 레포지토리 이름"), - ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), - ApiResponse(responseCode = "400", description = "잘못된 요청"), - ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ApiResponse( + description = "사용 가능한 레포지토리 이름인지 확인", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": { + "isAvailable": true + } + } + """, + ), + ), + ], + ), ], ) fun checkRepoName( name: String, member: Member, - ): ResponseEntity + ): com.ixfp.gitmon.common.type.ApiResponse @Operation(summary = "레포지토리 URL 조회") @ApiResponses( value = [ - ApiResponse(responseCode = "404", description = "레포지토리 URL 찾을 수 없음"), + ApiResponse( + description = "레포지토리 URL 조회", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": { + "githubRepoUrl": "https://api.gitmon.blog/api/v1/member/github/repo?githubUsername=kimsj-git" + } + } + """, + ), + ), + ], + ), ], ) - fun findGithubRepoUrl(githubUsername: String): ResponseEntity + fun findGithubRepoUrl(githubUsername: String): com.ixfp.gitmon.common.type.ApiResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index 329bb34..b2d6a08 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -38,28 +38,27 @@ interface IOauth2Controller { @ApiResponses( value = [ ApiResponse( - responseCode = "201", description = "로그인 및 토큰 발급 성공", content = [ Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - schema = Schema(implementation = AccessTokenResponse::class), + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": { + "accessToken": "header.payload.signature" + } + } + """, + ), ), ], ), - ApiResponse( - responseCode = "400", - description = "잘못된 요청 (code가 유효하지 않은 경우 등)", - ), - ApiResponse( - responseCode = "401", - description = "인증 실패", - ), - ApiResponse( - responseCode = "500", - description = "서버 에러", - ), + // TODO: 깃허브 API 에러로 인한 에러 응답 예제 추가 ], ) - fun login(request: GithubOauth2Request): ResponseEntity + fun login(request: GithubOauth2Request): com.ixfp.gitmon.common.type.ApiResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 515fe46..b811b98 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -3,10 +3,12 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.domain.member.Member 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.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity import org.springframework.web.multipart.MultipartFile @Tag(name = "Posting", description = "게시글(포스팅) 관련 API") @@ -16,9 +18,31 @@ interface IPostingController { description = "포스팅을 생성합니다.", security = [SecurityRequirement(name = ACCESS_TOKEN)], ) + @ApiResponses( + value = [ + ApiResponse( + description = "포스팅 생성 성공", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": null + } + """, + ), + ), + ], + ), + ], + ) fun createPosting( title: String, content: MultipartFile, member: Member, - ): ResponseEntity + ): com.ixfp.gitmon.common.type.ApiResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index e7305c0..7cabcf0 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,14 +1,15 @@ package com.ixfp.gitmon.controller +import com.ixfp.gitmon.common.type.ApiErrorType +import com.ixfp.gitmon.common.type.ApiResponse import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest +import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.domain.auth.AuthService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute @@ -28,26 +29,26 @@ class MemberController( override fun upsertRepo( @RequestBody request: CreateRepoRequest, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ResponseEntity { + ): ApiResponse { try { val githubAccessToken = authService.getGithubAccessToken(member.id) ?: throw AuthenticationException("사용자의 깃허브 토큰을 찾을 수 없음") if (member.repoName == request.name) { - return ResponseEntity(HttpStatus.OK) + return ApiResponse.success() } memberService.upsertRepo(member, request.name, githubAccessToken) - return ResponseEntity.status(HttpStatus.CREATED).build() + return ApiResponse.success() } catch (e: Exception) { log.error(e) { "Failed to create repository: ${e.message}" } - val status = + val errorType = when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - is AuthenticationException -> HttpStatus.UNAUTHORIZED - else -> HttpStatus.INTERNAL_SERVER_ERROR + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + is AuthenticationException -> ApiErrorType.UNAUTHORIZED + else -> ApiErrorType.INTERNAL_SERVER_ERROR } - return ResponseEntity.status(status).build() + return ApiResponse.error(errorType) } } @@ -55,37 +56,33 @@ class MemberController( override fun checkRepoName( @RequestParam name: String, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ResponseEntity { + ): ApiResponse { return try { val isAvailable = memberService.isRepoNameAvailable(member, name) - if (!isAvailable) { - ResponseEntity.status(HttpStatus.CONFLICT).build() - } else { - ResponseEntity.ok().build() - } + ApiResponse.success(CheckRepoNameResponse(isAvailable)) } catch (e: Exception) { log.error(e) { "Failed to check repository name: ${e.message}" } - val status = + val errorType = when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - is AuthenticationException -> HttpStatus.UNAUTHORIZED - else -> HttpStatus.INTERNAL_SERVER_ERROR + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + is AuthenticationException -> ApiErrorType.UNAUTHORIZED + else -> ApiErrorType.INTERNAL_SERVER_ERROR } - ResponseEntity.status(status).build() + ApiResponse.error(errorType) } } @GetMapping("/github/repo") override fun findGithubRepoUrl( @RequestParam githubUsername: String, - ): ResponseEntity { + ): ApiResponse { val githubRepoUrl = memberService.findGithubRepoUrl(githubUsername) - ?: return ResponseEntity.status(HttpStatus.NOT_FOUND).build() + ?: return ApiResponse.success() val response = GithubRepoUrlResponse(githubRepoUrl) - return ResponseEntity.status(HttpStatus.OK).body(response) + return ApiResponse.success(response) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index c376711..1718f06 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -1,6 +1,8 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.client.github.GithubApiService +import com.ixfp.gitmon.common.type.ApiErrorType +import com.ixfp.gitmon.common.type.ApiResponse import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse import com.ixfp.gitmon.domain.auth.AuthService @@ -33,7 +35,7 @@ class Oauth2Controller( @PostMapping("/login/oauth/github/tokens") override fun login( @RequestBody request: GithubOauth2Request, - ): ResponseEntity { + ): ApiResponse { try { val githubAccessToken = githubApiService.getAccessTokenByCode(request.code) val githubUser = githubApiService.getGithubUser(githubAccessToken) @@ -52,16 +54,16 @@ class Oauth2Controller( accessToken = accessToken, isRepoCreated = isRepoCreated, ) - return ResponseEntity.status(HttpStatus.CREATED).body(response) + return ApiResponse.success(response) } catch (e: Exception) { log.error(e) { "Failed to login: ${e.message}" } - val status = + val errorType = when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - is AuthenticationException -> HttpStatus.UNAUTHORIZED - else -> HttpStatus.INTERNAL_SERVER_ERROR + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + is AuthenticationException -> ApiErrorType.UNAUTHORIZED + else -> ApiErrorType.INTERNAL_SERVER_ERROR } - return ResponseEntity.status(status).build() + return ApiResponse.error(errorType) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 4f72899..b575320 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,12 +1,12 @@ package com.ixfp.gitmon.controller +import com.ixfp.gitmon.common.type.ApiErrorType +import com.ixfp.gitmon.common.type.ApiResponse import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestMapping @@ -27,18 +27,18 @@ class PostingController( @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ResponseEntity { + ): ApiResponse { try { postingService.create(member, title, content) - return ResponseEntity(HttpStatus.CREATED) + return ApiResponse.success() } catch (e: Exception) { - val status = + val errorType = when (e) { - is IllegalArgumentException -> HttpStatus.BAD_REQUEST - else -> HttpStatus.INTERNAL_SERVER_ERROR + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + else -> ApiErrorType.INTERNAL_SERVER_ERROR } log.error { "포스팅 생성 중 에러 발생: ${e.message}" } - return ResponseEntity(status) + return ApiResponse.error(errorType) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/CheckRepoNameResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/CheckRepoNameResponse.kt new file mode 100644 index 0000000..06dc3ff --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/CheckRepoNameResponse.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.controller.response + +data class CheckRepoNameResponse( + val isAvailable: Boolean, +) From 6b5d3aa0dee5ca92af61ad726726204bc1e18cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 22 Apr 2025 15:44:30 +0900 Subject: [PATCH 28/70] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=83=9D=EC=84=B1=EC=9E=90,=20=EC=9D=B8=EC=9E=90?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt index 6616795..e46cc9c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt @@ -5,16 +5,10 @@ data class ApiResponse( val data: T? = null, val error: ApiErrorType? = null, ) { - constructor(data: T) : this( - status = ApiStatus.SUCCESS, - data = data, - ) - companion object { fun success(): ApiResponse { return ApiResponse( status = ApiStatus.SUCCESS, - data = null, ) } From 708f42ac9a32bc5a023db107d55e5286a71f43a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 22 Apr 2025 15:48:54 +0900 Subject: [PATCH 29/70] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApiResponse -> ApiResponseBody --- .../{ApiResponse.kt => ApiResponseBody.kt} | 14 ++++++------ .../gitmon/controller/IMemberController.kt | 6 ++--- .../gitmon/controller/IOauth2Controller.kt | 2 +- .../gitmon/controller/IPostingController.kt | 2 +- .../gitmon/controller/MemberController.kt | 22 +++++++++---------- .../gitmon/controller/Oauth2Controller.kt | 8 +++---- .../gitmon/controller/PostingController.kt | 8 +++---- 7 files changed, 31 insertions(+), 31 deletions(-) rename src/main/kotlin/com/ixfp/gitmon/common/type/{ApiResponse.kt => ApiResponseBody.kt} (74%) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt similarity index 74% rename from src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt rename to src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt index e46cc9c..17f8da3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt @@ -1,26 +1,26 @@ package com.ixfp.gitmon.common.type -data class ApiResponse( +data class ApiResponseBody( val status: ApiStatus, val data: T? = null, val error: ApiErrorType? = null, ) { companion object { - fun success(): ApiResponse { - return ApiResponse( + fun success(): ApiResponseBody { + return ApiResponseBody( status = ApiStatus.SUCCESS, ) } - fun success(data: T): ApiResponse { - return ApiResponse( + fun success(data: T): ApiResponseBody { + return ApiResponseBody( status = ApiStatus.SUCCESS, data = data, ) } - fun error(error: ApiErrorType): ApiResponse { - return ApiResponse( + fun error(error: ApiErrorType): ApiResponseBody { + return ApiResponseBody( status = error.status, error = error, ) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index 49690a9..2f2c479 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -45,7 +45,7 @@ interface IMemberController { fun upsertRepo( request: CreateRepoRequest, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponse + ): com.ixfp.gitmon.common.type.ApiResponseBody @Operation( summary = "레포지토리 이름 중복 조회", @@ -79,7 +79,7 @@ interface IMemberController { fun checkRepoName( name: String, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponse + ): com.ixfp.gitmon.common.type.ApiResponseBody @Operation(summary = "레포지토리 URL 조회") @ApiResponses( @@ -106,5 +106,5 @@ interface IMemberController { ), ], ) - fun findGithubRepoUrl(githubUsername: String): com.ixfp.gitmon.common.type.ApiResponse + fun findGithubRepoUrl(githubUsername: String): com.ixfp.gitmon.common.type.ApiResponseBody } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index b2d6a08..51dc844 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -60,5 +60,5 @@ interface IOauth2Controller { // TODO: 깃허브 API 에러로 인한 에러 응답 예제 추가 ], ) - fun login(request: GithubOauth2Request): com.ixfp.gitmon.common.type.ApiResponse + fun login(request: GithubOauth2Request): com.ixfp.gitmon.common.type.ApiResponseBody } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index b811b98..cd8d80b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -44,5 +44,5 @@ interface IPostingController { title: String, content: MultipartFile, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponse + ): com.ixfp.gitmon.common.type.ApiResponseBody } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 7cabcf0..7d7cfc3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,7 +1,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponse +import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.CheckRepoNameResponse @@ -29,17 +29,17 @@ class MemberController( override fun upsertRepo( @RequestBody request: CreateRepoRequest, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponse { + ): ApiResponseBody { try { val githubAccessToken = authService.getGithubAccessToken(member.id) ?: throw AuthenticationException("사용자의 깃허브 토큰을 찾을 수 없음") if (member.repoName == request.name) { - return ApiResponse.success() + return ApiResponseBody.success() } memberService.upsertRepo(member, request.name, githubAccessToken) - return ApiResponse.success() + return ApiResponseBody.success() } catch (e: Exception) { log.error(e) { "Failed to create repository: ${e.message}" } val errorType = @@ -48,7 +48,7 @@ class MemberController( is AuthenticationException -> ApiErrorType.UNAUTHORIZED else -> ApiErrorType.INTERNAL_SERVER_ERROR } - return ApiResponse.error(errorType) + return ApiResponseBody.error(errorType) } } @@ -56,10 +56,10 @@ class MemberController( override fun checkRepoName( @RequestParam name: String, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponse { + ): ApiResponseBody { return try { val isAvailable = memberService.isRepoNameAvailable(member, name) - ApiResponse.success(CheckRepoNameResponse(isAvailable)) + ApiResponseBody.success(CheckRepoNameResponse(isAvailable)) } catch (e: Exception) { log.error(e) { "Failed to check repository name: ${e.message}" } val errorType = @@ -68,21 +68,21 @@ class MemberController( is AuthenticationException -> ApiErrorType.UNAUTHORIZED else -> ApiErrorType.INTERNAL_SERVER_ERROR } - ApiResponse.error(errorType) + ApiResponseBody.error(errorType) } } @GetMapping("/github/repo") override fun findGithubRepoUrl( @RequestParam githubUsername: String, - ): ApiResponse { + ): ApiResponseBody { val githubRepoUrl = memberService.findGithubRepoUrl(githubUsername) - ?: return ApiResponse.success() + ?: return ApiResponseBody.success() val response = GithubRepoUrlResponse(githubRepoUrl) - return ApiResponse.success(response) + return ApiResponseBody.success(response) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 1718f06..9d6edbf 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -2,7 +2,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponse +import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse import com.ixfp.gitmon.domain.auth.AuthService @@ -35,7 +35,7 @@ class Oauth2Controller( @PostMapping("/login/oauth/github/tokens") override fun login( @RequestBody request: GithubOauth2Request, - ): ApiResponse { + ): ApiResponseBody { try { val githubAccessToken = githubApiService.getAccessTokenByCode(request.code) val githubUser = githubApiService.getGithubUser(githubAccessToken) @@ -54,7 +54,7 @@ class Oauth2Controller( accessToken = accessToken, isRepoCreated = isRepoCreated, ) - return ApiResponse.success(response) + return ApiResponseBody.success(response) } catch (e: Exception) { log.error(e) { "Failed to login: ${e.message}" } val errorType = @@ -63,7 +63,7 @@ class Oauth2Controller( is AuthenticationException -> ApiErrorType.UNAUTHORIZED else -> ApiErrorType.INTERNAL_SERVER_ERROR } - return ApiResponse.error(errorType) + return ApiResponseBody.error(errorType) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index b575320..17d1ee4 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,7 +1,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponse +import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingService @@ -27,10 +27,10 @@ class PostingController( @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponse { + ): ApiResponseBody { try { postingService.create(member, title, content) - return ApiResponse.success() + return ApiResponseBody.success() } catch (e: Exception) { val errorType = when (e) { @@ -38,7 +38,7 @@ class PostingController( else -> ApiErrorType.INTERNAL_SERVER_ERROR } log.error { "포스팅 생성 중 에러 발생: ${e.message}" } - return ApiResponse.error(errorType) + return ApiResponseBody.error(errorType) } } From 789b749b2e3b9e4552c1028b305737df12343d09 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 26 Apr 2025 15:54:07 +0900 Subject: [PATCH 30/70] =?UTF-8?q?[issue#53]=20=EA=B0=9C=EB=B0=9C=EC=9A=A9?= =?UTF-8?q?=20github=20oauth=20client=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- src/main/resources/application.yml | 8 ++++++-- src/test/resources/application.yml | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9aedcbc..51edf67 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,8 +21,12 @@ spring: oauth2: client: github: - id: ${OAUTH2_CLIENT_GITHUB_ID} - secret: ${OAUTH2_CLIENT_GITHUB_SECRET} + id: ${GITHUB_OAUTH2_CLIENT_ID_PROD} + secret: ${GITHUB_OAUTH2_CLIENT_SECRET_PROD} + client-dev: + github: + id: ${GITHUB_OAUTH2_CLIENT_ID_DEV} + secret: ${GITHUB_OAUTH2_CLIENT_SECRET_DEV} jwt: secret: ${JWT_SECRET} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 62a0202..c284ab5 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -18,6 +18,10 @@ oauth2: github: id: 'id' secret: 'secret' + client-dev: + github: + id: 'id' + secret: 'dev' jwt: secret: 'jwt_secret_key_with_size_greater_than_256_bits' From d0b4a9b0222dc0d3784a5ed14aaa4e3a0e572f39 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 26 Apr 2025 15:57:18 +0900 Subject: [PATCH 31/70] =?UTF-8?q?[issue#53]=20=EA=B9=83=ED=97=88=EB=B8=8C?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20API=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20profile=20=EC=9D=84=20=EB=B0=9B=EC=95=84?= =?UTF-8?q?=EC=84=9C=20dev/prod=20=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=84=9C=EB=A1=9C=EB=8B=A4=EB=A5=B8=20oauth=20app=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/GitmonApplication.kt | 2 + .../gitmon/client/github/GithubApiService.kt | 42 +++++++++++++++---- .../github/GithubOauth2ClientDevProperties.kt | 9 ++++ .../GithubOauth2ClientProdProperties.kt | 9 ++++ .../com/ixfp/gitmon/common/type/Profile.kt | 13 ++++++ .../gitmon/controller/IOauth2Controller.kt | 7 +++- .../gitmon/controller/Oauth2Controller.kt | 13 +++--- 7 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientDevProperties.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientProdProperties.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/type/Profile.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt b/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt index c76d234..bf3a5f5 100644 --- a/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt +++ b/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt @@ -1,11 +1,13 @@ package com.ixfp.gitmon import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication import org.springframework.cloud.openfeign.EnableFeignClients @SpringBootApplication @EnableFeignClients +@ConfigurationPropertiesScan class GitmonApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 650aa76..db35ba8 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -4,10 +4,10 @@ import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubContent import com.ixfp.gitmon.client.github.response.GithubUserResponse +import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken import io.github.oshai.kotlinlogging.KotlinLogging.logger -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile @@ -15,20 +15,30 @@ import org.springframework.web.multipart.MultipartFile class GithubApiService( private val githubOauth2ApiClient: GithubOauth2ApiClient, private val githubResourceApiClient: GithubResourceApiClient, - @Value("\${oauth2.client.github.id}") private val githubClientId: String, - @Value("\${oauth2.client.github.secret}") private val githubClientSecret: String, + private val githubProdClient: GithubOauth2ClientProdProperties, + private val githubDevClient: GithubOauth2ClientDevProperties, ) { fun getGithubUser(githubAccessToken: String): GithubUserResponse { return githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) } - fun getAccessTokenByCode(code: String): String { - log.info { "GithubApiService#getAccessTokenByCode start. code=$code" } + fun getAuthRedirectionUrl(profile: Profile): String { + return when (profile) { + Profile.PROD -> buildAuthRedirectionUrl(githubProdClient.id) + Profile.DEV -> buildAuthRedirectionUrl(githubDevClient.id) + } + } + + fun getAccessTokenByCode( + code: String, + profile: Profile, + ): String { + log.info { "GithubApiService#getAccessTokenByCode start. code=$code, profile=$profile" } val request = GithubAccessTokenRequest( code = code, - client_id = githubClientId, - client_secret = githubClientSecret, + client_id = githubClientId(profile), + client_secret = githubClientSecret(profile), ) val response = githubOauth2ApiClient.fetchAccessToken(request) log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } @@ -64,6 +74,24 @@ class GithubApiService( return response.content } + private fun buildAuthRedirectionUrl(githubClientId: String): String { + return "https://github.com/login/oauth/authorize?&scope=repo&client_id=$githubClientId" + } + + private fun githubClientId(profile: Profile): String { + return when (profile) { + Profile.PROD -> githubProdClient.id + Profile.DEV -> githubDevClient.id + } + } + + private fun githubClientSecret(profile: Profile): String { + return when (profile) { + Profile.PROD -> githubProdClient.secret + Profile.DEV -> githubDevClient.secret + } + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientDevProperties.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientDevProperties.kt new file mode 100644 index 0000000..a169f6b --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientDevProperties.kt @@ -0,0 +1,9 @@ +package com.ixfp.gitmon.client.github + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "oauth2.client-dev.github") +data class GithubOauth2ClientDevProperties( + val id: String, + val secret: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientProdProperties.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientProdProperties.kt new file mode 100644 index 0000000..d491325 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubOauth2ClientProdProperties.kt @@ -0,0 +1,9 @@ +package com.ixfp.gitmon.client.github + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "oauth2.client.github") +data class GithubOauth2ClientProdProperties( + val id: String, + val secret: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/type/Profile.kt b/src/main/kotlin/com/ixfp/gitmon/common/type/Profile.kt new file mode 100644 index 0000000..0e7944f --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/type/Profile.kt @@ -0,0 +1,13 @@ +package com.ixfp.gitmon.common.type + +enum class Profile(val code: String, val description: String) { + DEV("dev", "개발환경"), + PROD("prod", "운영환경"), + ; + + companion object { + fun findByCode(code: String?): Profile { + return entries.find { it.code == code } ?: PROD + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index b2d6a08..9576fc6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -29,7 +29,7 @@ interface IOauth2Controller { ), ], ) - fun redirectToGithubOauthUrl(): ResponseEntity + fun redirectToGithubOauthUrl(profile: String?): ResponseEntity @Operation( summary = "gitmon Access 토큰 발급", @@ -60,5 +60,8 @@ interface IOauth2Controller { // TODO: 깃허브 API 에러로 인한 에러 응답 예제 추가 ], ) - fun login(request: GithubOauth2Request): com.ixfp.gitmon.common.type.ApiResponse + fun login( + profile: String?, + request: GithubOauth2Request, + ): com.ixfp.gitmon.common.type.ApiResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 1718f06..945b97a 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -3,41 +3,44 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.common.type.ApiErrorType import com.ixfp.gitmon.common.type.ApiResponse +import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse import com.ixfp.gitmon.domain.auth.AuthService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import javax.naming.AuthenticationException @RestController @RequestMapping("/api/v1") class Oauth2Controller( - @Value("\${oauth2.client.github.id}") private val githubClientId: String, private val authService: AuthService, private val githubApiService: GithubApiService, ) : IOauth2Controller { @GetMapping("/login/oauth/github") - override fun redirectToGithubOauthUrl(): ResponseEntity { + override fun redirectToGithubOauthUrl(profile: String?): ResponseEntity { + val redirectionUrl = githubApiService.getAuthRedirectionUrl(Profile.findByCode(profile)) + return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header( "Location", - "https://github.com/login/oauth/authorize?&scope=repo&client_id=$githubClientId", + redirectionUrl, ).build() } @PostMapping("/login/oauth/github/tokens") override fun login( + @RequestParam profile: String?, @RequestBody request: GithubOauth2Request, ): ApiResponse { try { - val githubAccessToken = githubApiService.getAccessTokenByCode(request.code) + val githubAccessToken = githubApiService.getAccessTokenByCode(request.code, Profile.findByCode(profile)) val githubUser = githubApiService.getGithubUser(githubAccessToken) val member = authService.getMemberByGithubId(githubUser.id.toLong()) From 5106ec91b29d6041634bb176c5050c010ea7cf11 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 27 Apr 2025 21:16:46 +0900 Subject: [PATCH 32/70] =?UTF-8?q?[issue#49]=20posting=20repository=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../ixfp/gitmon/db/posting/PostingEntity.kt | 21 +++++++++++++++++++ .../gitmon/db/posting/PostingRepository.kt | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt new file mode 100644 index 0000000..15c20df --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt @@ -0,0 +1,21 @@ +package com.ixfp.gitmon.db.posting + +import com.ixfp.gitmon.db.BaseEntity +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "posting") +class PostingEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + val title: String, + val refMemberId: Long, + val githubFilePath: String, + val githubFileSha: String, + val githubDownloadUrl: String, +) : BaseEntity() diff --git a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt new file mode 100644 index 0000000..13234f0 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.db.posting + +import org.springframework.data.jpa.repository.JpaRepository + +interface PostingRepository : JpaRepository From b061f1a6c633785cfbe081ccf749d06c2f20eb59 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 27 Apr 2025 21:27:03 +0900 Subject: [PATCH 33/70] =?UTF-8?q?[issue#49]=20posting=20=EB=82=B4=EC=97=AD?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../com/ixfp/gitmon/domain/posting/Posting.kt | 11 ++++++++++ .../gitmon/domain/posting/PostingService.kt | 14 +++++++++++- .../gitmon/domain/posting/PostingWriter.kt | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt new file mode 100644 index 0000000..7bdc2b9 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt @@ -0,0 +1,11 @@ +package com.ixfp.gitmon.domain.posting + +import com.ixfp.gitmon.domain.member.Member + +data class Posting( + val title: String, + val member: Member, + val githubFilePath: String, + val githubFileSha: String, + val githubDownloadUrl: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index e19efb2..521ef60 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -12,6 +12,7 @@ import java.util.UUID class PostingService( private val memberReader: MemberReader, private val githubApiService: GithubApiService, + private val postingWriter: PostingWriter, ) { fun create( member: Member, @@ -35,7 +36,18 @@ class PostingService( repo = member.repoName, path = "$title.md", ) - log.info { "PostingService#create end. contentSha=$githubContent.sha" } + + postingWriter.write( + Posting( + title = title, + member = member, + githubFilePath = githubContent.path, + githubFileSha = githubContent.sha, + githubDownloadUrl = githubContent.download_url, + ), + ) + + log.info { "PostingService#create end. contentSha=${githubContent.sha}" } } private fun uploadImage( diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt new file mode 100644 index 0000000..d54f8d0 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt @@ -0,0 +1,22 @@ +package com.ixfp.gitmon.domain.posting + +import com.ixfp.gitmon.db.posting.PostingEntity +import com.ixfp.gitmon.db.posting.PostingRepository +import org.springframework.stereotype.Component + +@Component +class PostingWriter( + private val postingRepository: PostingRepository, +) { + fun write(posting: Posting) { + val entity = + PostingEntity( + title = posting.title, + refMemberId = posting.member.id, + githubFilePath = posting.githubFilePath, + githubFileSha = posting.githubFileSha, + githubDownloadUrl = posting.githubDownloadUrl, + ) + postingRepository.save(entity) + } +} From ac11224be3ebbaf001af5925e0907e210abe15f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Thu, 1 May 2025 14:21:35 +0900 Subject: [PATCH 34/70] =?UTF-8?q?refactor:=20=EC=95=B1=20=EC=98=88?= =?UTF-8?q?=EC=99=B8,=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ixfp/gitmon/common/exception/AppException.kt | 3 +++ .../com/ixfp/gitmon/common/exception/LayerExceptions.kt | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/exception/AppException.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/exception/LayerExceptions.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/common/exception/AppException.kt b/src/main/kotlin/com/ixfp/gitmon/common/exception/AppException.kt new file mode 100644 index 0000000..d2832f4 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/exception/AppException.kt @@ -0,0 +1,3 @@ +package com.ixfp.gitmon.common.exception + +open class AppException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/exception/LayerExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/common/exception/LayerExceptions.kt new file mode 100644 index 0000000..b7b1c80 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/exception/LayerExceptions.kt @@ -0,0 +1,7 @@ +package com.ixfp.gitmon.common.exception + +open class PresentationException(message: String, cause: Throwable? = null) : AppException(message, cause) + +open class DomainException(message: String, cause: Throwable? = null) : AppException(message, cause) + +open class RepositoryException(message: String, cause: Throwable? = null) : AppException(message, cause) From c74713695e70506d9491b2bf49760f22c0651730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Thu, 1 May 2025 14:22:36 +0900 Subject: [PATCH 35/70] =?UTF-8?q?refactor:=20=EA=B9=83=ED=97=88=EB=B8=8C?= =?UTF-8?q?=20API=20=EC=98=88=EC=99=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/client/github/GithubApiService.kt | 61 ++++++++++++------- .../github/exception/GithubApiExceptions.kt | 28 +++++++++ 2 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptions.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 650aa76..d3a3be1 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -1,5 +1,11 @@ package com.ixfp.gitmon.client.github +import com.fasterxml.jackson.core.JsonProcessingException +import com.ixfp.gitmon.client.github.exception.GithubApiException +import com.ixfp.gitmon.client.github.exception.GithubInvalidAuthCodeException +import com.ixfp.gitmon.client.github.exception.GithubNetworkException +import com.ixfp.gitmon.client.github.exception.GithubResponseParsingException +import com.ixfp.gitmon.client.github.exception.GithubUnexpectedException import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubContent @@ -10,6 +16,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile +import java.io.IOException @Component class GithubApiService( @@ -19,23 +26,17 @@ class GithubApiService( @Value("\${oauth2.client.github.secret}") private val githubClientSecret: String, ) { fun getGithubUser(githubAccessToken: String): GithubUserResponse { - return githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) + return runCatchingGithub { + githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) + } } fun getAccessTokenByCode(code: String): String { - log.info { "GithubApiService#getAccessTokenByCode start. code=$code" } - val request = - GithubAccessTokenRequest( - code = code, - client_id = githubClientId, - client_secret = githubClientSecret, - ) - val response = githubOauth2ApiClient.fetchAccessToken(request) - log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } - if (response.accessToken == null) { - throw RuntimeException("Failed to get access token from github. response=$response") + val request = GithubAccessTokenRequest(code, githubClientId, githubClientSecret) + return runCatchingGithub { + val response = githubOauth2ApiClient.fetchAccessToken(request) + response.accessToken ?: throw GithubInvalidAuthCodeException("response=$response") } - return response.accessToken } fun upsertFile( @@ -52,19 +53,35 @@ class GithubApiService( content = Base64Encoder.encodeBase64(content), sha = "", ) - val response = - githubResourceApiClient.upsertFile( - bearerToken = BearerToken(githubAccessToken).format(), - owner = githubUsername, - repo = repo, - path = path, - request = request, - ) + return runCatchingGithub { + val response = + githubResourceApiClient.upsertFile( + bearerToken = BearerToken(githubAccessToken).format(), + owner = githubUsername, + repo = repo, + path = path, + request = request, + ) + response.content + } + } - return response.content + private inline fun runCatchingGithub(block: () -> T): T { + return try { + block() + } catch (ex: GithubApiException) { + throw ex + } catch (ex: IOException) { + throw GithubNetworkException(cause = ex) + } catch (ex: JsonProcessingException) { + throw GithubResponseParsingException(cause = ex) + } catch (ex: Exception) { + throw GithubUnexpectedException(cause = ex) + } } companion object { private val log = logger {} } } + diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptions.kt new file mode 100644 index 0000000..26fdbca --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptions.kt @@ -0,0 +1,28 @@ +package com.ixfp.gitmon.client.github.exception + +import com.ixfp.gitmon.common.exception.AppException + +sealed class GithubApiException( + message: String, + cause: Throwable? = null, +) : AppException(message, cause) + +class GithubNetworkException( + message: String = "GitHub 서버와의 네트워크 통신에 실패했습니다.", + cause: Throwable? = null, +) : GithubApiException(message, cause) + +class GithubResponseParsingException( + message: String = "GitHub 응답을 파싱하는 데 실패했습니다.", + cause: Throwable? = null, +) : GithubApiException(message, cause) + +class GithubUnexpectedException( + message: String = "GitHub API 호출 중 예상치 못한 예외가 발생했습니다.", + cause: Throwable? = null, +) : GithubApiException(message, cause) + +class GithubInvalidAuthCodeException( + message: String = "Github Auth Code가 유효하지 않습니다.", + cause: Throwable? = null, +) : GithubApiException(message, cause) From af6456e0c12869717c3d69c557910ecf49356f4c Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 3 May 2025 10:26:49 +0900 Subject: [PATCH 36/70] =?UTF-8?q?[issue#50]=20posting=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Suhjeong Kim --- .../gitmon/controller/PostingController.kt | 21 ++++++++++++++++ .../gitmon/db/posting/PostingRepository.kt | 4 +++- .../gitmon/domain/posting/PostingReadDto.kt | 11 +++++++++ .../gitmon/domain/posting/PostingReader.kt | 24 +++++++++++++++++++ .../gitmon/domain/posting/PostingService.kt | 10 +++++++- .../{Posting.kt => PostingWriteDto.kt} | 2 +- .../gitmon/domain/posting/PostingWriter.kt | 12 +++++----- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReadDto.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt rename src/main/kotlin/com/ixfp/gitmon/domain/posting/{Posting.kt => PostingWriteDto.kt} (89%) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index b575320..19695d7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -4,9 +4,12 @@ import com.ixfp.gitmon.common.type.ApiErrorType import com.ixfp.gitmon.common.type.ApiResponse import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.domain.member.Member +import com.ixfp.gitmon.domain.posting.PostingReadDto import com.ixfp.gitmon.domain.posting.PostingService import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestMapping @@ -42,6 +45,24 @@ class PostingController( } } + @GetMapping("/{exposedMemberId}") + fun getPostingList( + @PathVariable exposedMemberId: String, + ): ApiResponse> { + try { + val postingList = postingService.findPostingListByMemberExposedId(exposedMemberId) + return ApiResponse.success(postingList) + } catch (e: Exception) { + val errorType = + when (e) { + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + else -> ApiErrorType.INTERNAL_SERVER_ERROR + } + log.error { "포스팅 목록 조회 중 에러 발생: ${e.message}" } + return ApiResponse.error(errorType) + } + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt index 13234f0..02c40f5 100644 --- a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt +++ b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingRepository.kt @@ -2,4 +2,6 @@ package com.ixfp.gitmon.db.posting import org.springframework.data.jpa.repository.JpaRepository -interface PostingRepository : JpaRepository +interface PostingRepository : JpaRepository { + fun findByRefMemberId(refMemberId: Long): List +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReadDto.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReadDto.kt new file mode 100644 index 0000000..e21121c --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReadDto.kt @@ -0,0 +1,11 @@ +package com.ixfp.gitmon.domain.posting + +import java.time.LocalDateTime + +data class PostingReadDto( + val id: Long, + val title: String, + val githubDownloadUrl: String, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt new file mode 100644 index 0000000..f7cb1bc --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt @@ -0,0 +1,24 @@ +package com.ixfp.gitmon.domain.posting + +import com.ixfp.gitmon.db.posting.PostingEntity +import com.ixfp.gitmon.db.posting.PostingRepository +import org.springframework.stereotype.Component + +@Component +class PostingReader( + private val postingRepository: PostingRepository, +) { + fun findPostingListByMemberId(memberId: Long): List { + return postingRepository.findByRefMemberId(memberId).map { toPostingItem(it) } + } + + private fun toPostingItem(entity: PostingEntity): PostingReadDto { + return PostingReadDto( + id = entity.id, + title = entity.title, + githubDownloadUrl = entity.githubDownloadUrl, + createdAt = entity.createdAt, + updatedAt = entity.updatedAt, + ) + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 521ef60..8a3c078 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -12,8 +12,16 @@ import java.util.UUID class PostingService( private val memberReader: MemberReader, private val githubApiService: GithubApiService, + private val postingReader: PostingReader, private val postingWriter: PostingWriter, ) { + fun findPostingListByMemberExposedId(memberExposedId: String): List { + val member = + memberReader.findByExposedId(memberExposedId) + ?: throw IllegalArgumentException("존재하지 않는 회원입니다.") + return postingReader.findPostingListByMemberId(member.id) + } + fun create( member: Member, title: String, @@ -38,7 +46,7 @@ class PostingService( ) postingWriter.write( - Posting( + PostingWriteDto( title = title, member = member, githubFilePath = githubContent.path, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriteDto.kt similarity index 89% rename from src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt rename to src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriteDto.kt index 7bdc2b9..9571c5b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriteDto.kt @@ -2,7 +2,7 @@ package com.ixfp.gitmon.domain.posting import com.ixfp.gitmon.domain.member.Member -data class Posting( +data class PostingWriteDto( val title: String, val member: Member, val githubFilePath: String, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt index d54f8d0..099c329 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt @@ -8,14 +8,14 @@ import org.springframework.stereotype.Component class PostingWriter( private val postingRepository: PostingRepository, ) { - fun write(posting: Posting) { + fun write(postingWriteDto: PostingWriteDto) { val entity = PostingEntity( - title = posting.title, - refMemberId = posting.member.id, - githubFilePath = posting.githubFilePath, - githubFileSha = posting.githubFileSha, - githubDownloadUrl = posting.githubDownloadUrl, + title = postingWriteDto.title, + refMemberId = postingWriteDto.member.id, + githubFilePath = postingWriteDto.githubFilePath, + githubFileSha = postingWriteDto.githubFileSha, + githubDownloadUrl = postingWriteDto.githubDownloadUrl, ) postingRepository.save(entity) } From 3f1b380da653f56b917d15e0bc0ba762d81587ad Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 3 May 2025 10:59:25 +0900 Subject: [PATCH 37/70] =?UTF-8?q?[issue#50]=20posting=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index 90f55fd..8d52212 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -12,7 +12,7 @@ class WebMvcConfig( override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 - .addPathPatterns("/api/v1/posting/**") + .addPathPatterns("/api/v1/posting", "/api/v1/posting/") } override fun addCorsMappings(registry: CorsRegistry) { From 45b64e144c54ad7cbd7d0a81c28c8bd29fe8fc0e Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 3 May 2025 11:23:24 +0900 Subject: [PATCH 38/70] =?UTF-8?q?[issue#51]=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ixfp/gitmon/config/web/WebMvcConfig.kt | 1 + .../gitmon/controller/IMemberController.kt | 36 +++++++++++++++++++ .../gitmon/controller/MemberController.kt | 29 +++++++++++++++ .../controller/response/MemberInfoResponse.kt | 8 +++++ 4 files changed, 74 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/response/MemberInfoResponse.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index 8d52212..08e7822 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -12,6 +12,7 @@ class WebMvcConfig( override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 + .addPathPatterns("/api/v1/member/*") .addPathPatterns("/api/v1/posting", "/api/v1/posting/") } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index 49690a9..2c9db22 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -4,6 +4,7 @@ import com.ixfp.gitmon.config.docs.ACCESS_TOKEN import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse +import com.ixfp.gitmon.controller.response.MemberInfoResponse import com.ixfp.gitmon.domain.member.Member import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content @@ -15,6 +16,41 @@ import io.swagger.v3.oas.annotations.tags.Tag @Tag(name = "Member", description = "회원 관련 API") interface IMemberController { + @Operation( + summary = "회원 정보 조회", + security = [SecurityRequirement(name = ACCESS_TOKEN)], + ) + @ApiResponses( + value = [ + ApiResponse( + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": { + "id": "exposedId", + "githubUsername": "kimsj-git", + "isRepoCreated": "true", + "repoName": "gitmon" + } + } + """, + ), + ), + ], + ), + ], + ) + fun getMember( + exposedMemberId: String, + member: Member, + ): com.ixfp.gitmon.common.type.ApiResponse + @Operation( summary = "레포지토리 생성/갱신", description = "이미 설정된 레포지토리가 있다면 갱신하고, 없다면 새로 생성합니다.", diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 7cabcf0..b6b35be 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -6,11 +6,13 @@ import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse +import com.ixfp.gitmon.controller.response.MemberInfoResponse import com.ixfp.gitmon.domain.auth.AuthService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody @@ -25,6 +27,33 @@ class MemberController( private val authService: AuthService, private val memberService: MemberService, ) : IMemberController { + @GetMapping("/{exposedMemberId}") + override fun getMember( + @PathVariable exposedMemberId: String, + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, + ): ApiResponse { + return try { + val response = + MemberInfoResponse( + id = member.exposedId, + githubUsername = member.githubUsername, + repoName = member.repoName, + isRepoCreated = member.repoName != null, + ) + + ApiResponse.success(response) + } catch (e: Exception) { + log.error(e) { "Failed to get member: ${e.message}" } + val errorType = + when (e) { + is IllegalArgumentException -> ApiErrorType.BAD_REQUEST + is AuthenticationException -> ApiErrorType.UNAUTHORIZED + else -> ApiErrorType.INTERNAL_SERVER_ERROR + } + ApiResponse.error(errorType) + } + } + @PostMapping("/repo") override fun upsertRepo( @RequestBody request: CreateRepoRequest, diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/MemberInfoResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/MemberInfoResponse.kt new file mode 100644 index 0000000..9b5f7fa --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/MemberInfoResponse.kt @@ -0,0 +1,8 @@ +package com.ixfp.gitmon.controller.response + +data class MemberInfoResponse( + val id: String, + val githubUsername: String, + val isRepoCreated: Boolean, + val repoName: String?, +) From d1c15cf42c1063fc1bf8407e9c52ddeaab0046be Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 3 May 2025 19:30:52 +0900 Subject: [PATCH 39/70] =?UTF-8?q?[issue#51]=20=ED=9A=8C=EC=9B=90=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt | 2 +- .../kotlin/com/ixfp/gitmon/controller/IMemberController.kt | 5 +---- .../kotlin/com/ixfp/gitmon/controller/MemberController.kt | 5 +---- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index 08e7822..b0e2787 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -12,7 +12,7 @@ class WebMvcConfig( override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 - .addPathPatterns("/api/v1/member/*") + .addPathPatterns("/api/v1/member", "/api/v1/member/") .addPathPatterns("/api/v1/posting", "/api/v1/posting/") } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index 2c9db22..1d8ef16 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -46,10 +46,7 @@ interface IMemberController { ), ], ) - fun getMember( - exposedMemberId: String, - member: Member, - ): com.ixfp.gitmon.common.type.ApiResponse + fun getMember(member: Member): com.ixfp.gitmon.common.type.ApiResponse @Operation( summary = "레포지토리 생성/갱신", diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index b6b35be..13c6f65 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -12,7 +12,6 @@ import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestBody @@ -27,9 +26,8 @@ class MemberController( private val authService: AuthService, private val memberService: MemberService, ) : IMemberController { - @GetMapping("/{exposedMemberId}") + @GetMapping override fun getMember( - @PathVariable exposedMemberId: String, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ApiResponse { return try { @@ -40,7 +38,6 @@ class MemberController( repoName = member.repoName, isRepoCreated = member.repoName != null, ) - ApiResponse.success(response) } catch (e: Exception) { log.error(e) { "Failed to get member: ${e.message}" } From d8d40da0e97389a4b8ecd7540483320e63b23a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 12:17:37 +0900 Subject: [PATCH 40/70] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=9E=98=ED=95=91=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ixfp/gitmon/GitmonApplication.kt | 2 ++ .../common/aop/ExceptionWrappingAspect.kt | 27 +++++++++++++++++++ .../common/aop/ExceptionWrappingStrategy.kt | 5 ++++ .../com/ixfp/gitmon/common/aop/WrapWith.kt | 9 +++++++ 4 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/aop/WrapWith.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt b/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt index bf3a5f5..67ac969 100644 --- a/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt +++ b/src/main/kotlin/com/ixfp/gitmon/GitmonApplication.kt @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.EnableAspectJAutoProxy @SpringBootApplication @EnableFeignClients @ConfigurationPropertiesScan +@EnableAspectJAutoProxy class GitmonApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt new file mode 100644 index 0000000..fc6382d --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt @@ -0,0 +1,27 @@ +package com.ixfp.gitmon.common.aop + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.springframework.context.ApplicationContext +import org.springframework.stereotype.Component + +@Aspect +@Component +class ExceptionWrappingAspect( + private val applicationContext: ApplicationContext, +) { + @Around("@annotation(wrapWith)") + fun aroundAnnotated( + joinPoint: ProceedingJoinPoint, + wrapWith: WrapWith, + ): Any? { + return try { + joinPoint.proceed() + } catch (original: Throwable) { + val strategyClass = wrapWith.value.java + val strategy = applicationContext.getBean(strategyClass) + throw strategy.wrap(original) + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt new file mode 100644 index 0000000..bfe2e04 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.common.aop + +interface ExceptionWrappingStrategy { + fun wrap(e: Throwable): RuntimeException +} diff --git a/src/main/kotlin/com/ixfp/gitmon/common/aop/WrapWith.kt b/src/main/kotlin/com/ixfp/gitmon/common/aop/WrapWith.kt new file mode 100644 index 0000000..f2c6e42 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/aop/WrapWith.kt @@ -0,0 +1,9 @@ +package com.ixfp.gitmon.common.aop + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class WrapWith( + val value: KClass, +) From 8f857dfab34036d8de37a4264cc9169f04d8bc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 12:17:59 +0900 Subject: [PATCH 41/70] =?UTF-8?q?refactor:=20=EA=B9=83=ED=97=88=EB=B8=8C?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=9E=98=ED=95=91=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/client/github/GithubApiService.kt | 62 +++++++------------ .../exception/GithubApiExceptionStrategy.kt | 17 +++++ 2 files changed, 38 insertions(+), 41 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 06e1a23..f3fe849 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -1,21 +1,17 @@ package com.ixfp.gitmon.client.github -import com.fasterxml.jackson.core.JsonProcessingException -import com.ixfp.gitmon.client.github.exception.GithubApiException -import com.ixfp.gitmon.client.github.exception.GithubNetworkException -import com.ixfp.gitmon.client.github.exception.GithubResponseParsingException -import com.ixfp.gitmon.client.github.exception.GithubUnexpectedException +import com.ixfp.gitmon.client.github.exception.GithubApiExceptionStrategy import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubContent import com.ixfp.gitmon.client.github.response.GithubUserResponse +import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile -import java.io.IOException @Component class GithubApiService( @@ -24,12 +20,12 @@ class GithubApiService( private val githubProdClient: GithubOauth2ClientProdProperties, private val githubDevClient: GithubOauth2ClientDevProperties, ) { + @WrapWith(GithubApiExceptionStrategy::class) fun getGithubUser(githubAccessToken: String): GithubUserResponse { - return runCatchingGithub { - githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) - } + return githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) } + @WrapWith(GithubApiExceptionStrategy::class) fun getAuthRedirectionUrl(profile: Profile): String { return when (profile) { Profile.PROD -> buildAuthRedirectionUrl(githubProdClient.id) @@ -37,6 +33,7 @@ class GithubApiService( } } + @WrapWith(GithubApiExceptionStrategy::class) fun getAccessTokenByCode( code: String, profile: Profile, @@ -49,16 +46,15 @@ class GithubApiService( client_secret = githubClientSecret(profile), ) - runCatchingGithub { - val response = githubOauth2ApiClient.fetchAccessToken(request) - log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } - if (response.accessToken == null) { - throw RuntimeException("Failed to get access token from github. response=$response") - } - return response.accessToken + val response = githubOauth2ApiClient.fetchAccessToken(request) + log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } + if (response.accessToken == null) { + throw RuntimeException("Failed to get access token from github. response=$response") } + return response.accessToken } + @WrapWith(GithubApiExceptionStrategy::class) fun upsertFile( githubAccessToken: String, content: MultipartFile, @@ -73,31 +69,15 @@ class GithubApiService( content = Base64Encoder.encodeBase64(content), sha = "", ) - return runCatchingGithub { - val response = - githubResourceApiClient.upsertFile( - bearerToken = BearerToken(githubAccessToken).format(), - owner = githubUsername, - repo = repo, - path = path, - request = request, - ) - response.content - } - } - - private inline fun runCatchingGithub(block: () -> T): T { - return try { - block() - } catch (ex: GithubApiException) { - throw ex - } catch (ex: IOException) { - throw GithubNetworkException(cause = ex) - } catch (ex: JsonProcessingException) { - throw GithubResponseParsingException(cause = ex) - } catch (ex: Exception) { - throw GithubUnexpectedException(cause = ex) - } + val response = + githubResourceApiClient.upsertFile( + bearerToken = BearerToken(githubAccessToken).format(), + owner = githubUsername, + repo = repo, + path = path, + request = request, + ) + return response.content } private fun buildAuthRedirectionUrl(githubClientId: String): String { diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt new file mode 100644 index 0000000..bbf49a8 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt @@ -0,0 +1,17 @@ +package com.ixfp.gitmon.client.github.exception + +import com.fasterxml.jackson.core.JsonProcessingException +import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import org.springframework.stereotype.Component +import java.io.IOException + +@Component +class GithubApiExceptionStrategy : ExceptionWrappingStrategy { + override fun wrap(e: Throwable): RuntimeException = + when (e) { + is GithubApiException -> e + is JsonProcessingException -> GithubResponseParsingException(cause = e) + is IOException -> GithubNetworkException(cause = e) + else -> GithubUnexpectedException(cause = e) + } +} From 2888617ba9f5a369808a0ad8669b55b9e4ca6f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 12:20:51 +0900 Subject: [PATCH 42/70] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index f3fe849..30c9cda 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -1,6 +1,7 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.exception.GithubApiExceptionStrategy +import com.ixfp.gitmon.client.github.exception.GithubInvalidAuthCodeException import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubContent @@ -38,7 +39,6 @@ class GithubApiService( code: String, profile: Profile, ): String { - log.info { "GithubApiService#getAccessTokenByCode start. code=$code, profile=$profile" } val request = GithubAccessTokenRequest( code = code, @@ -47,9 +47,8 @@ class GithubApiService( ) val response = githubOauth2ApiClient.fetchAccessToken(request) - log.info { "GithubApiService#getAccessTokenByCode end. response=$response" } if (response.accessToken == null) { - throw RuntimeException("Failed to get access token from github. response=$response") + throw GithubInvalidAuthCodeException("response=$response") } return response.accessToken } From 9f522f9a26cd621c47a9121bd614fefa0ae0a97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 12:26:08 +0900 Subject: [PATCH 43/70] =?UTF-8?q?refactor:=20wrap=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=EC=9D=98=20throw=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20App?= =?UTF-8?q?Exception=EC=9C=BC=EB=A1=9C=20=ED=95=9C=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/github/exception/GithubApiExceptionStrategy.kt | 3 ++- .../com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt index bbf49a8..4ab3bae 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/exception/GithubApiExceptionStrategy.kt @@ -2,12 +2,13 @@ package com.ixfp.gitmon.client.github.exception import com.fasterxml.jackson.core.JsonProcessingException import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import com.ixfp.gitmon.common.exception.AppException import org.springframework.stereotype.Component import java.io.IOException @Component class GithubApiExceptionStrategy : ExceptionWrappingStrategy { - override fun wrap(e: Throwable): RuntimeException = + override fun wrap(e: Throwable): AppException = when (e) { is GithubApiException -> e is JsonProcessingException -> GithubResponseParsingException(cause = e) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt index bfe2e04..674b825 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingStrategy.kt @@ -1,5 +1,7 @@ package com.ixfp.gitmon.common.aop +import com.ixfp.gitmon.common.exception.AppException + interface ExceptionWrappingStrategy { - fun wrap(e: Throwable): RuntimeException + fun wrap(e: Throwable): AppException } From 936b0b2a8a306767362323baaec7307e79a5795d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 13:14:27 +0900 Subject: [PATCH 44/70] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=EB=8F=84=20=EC=A0=81=EC=9A=A9=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/client/github/GithubApiService.kt | 5 +---- .../common/aop/ExceptionWrappingAspect.kt | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 30c9cda..3042e2b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -14,6 +14,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile +@WrapWith(GithubApiExceptionStrategy::class) @Component class GithubApiService( private val githubOauth2ApiClient: GithubOauth2ApiClient, @@ -21,12 +22,10 @@ class GithubApiService( private val githubProdClient: GithubOauth2ClientProdProperties, private val githubDevClient: GithubOauth2ClientDevProperties, ) { - @WrapWith(GithubApiExceptionStrategy::class) fun getGithubUser(githubAccessToken: String): GithubUserResponse { return githubResourceApiClient.fetchUser(BearerToken(githubAccessToken).format()) } - @WrapWith(GithubApiExceptionStrategy::class) fun getAuthRedirectionUrl(profile: Profile): String { return when (profile) { Profile.PROD -> buildAuthRedirectionUrl(githubProdClient.id) @@ -34,7 +33,6 @@ class GithubApiService( } } - @WrapWith(GithubApiExceptionStrategy::class) fun getAccessTokenByCode( code: String, profile: Profile, @@ -53,7 +51,6 @@ class GithubApiService( return response.accessToken } - @WrapWith(GithubApiExceptionStrategy::class) fun upsertFile( githubAccessToken: String, content: MultipartFile, diff --git a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt index fc6382d..37bb72b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/aop/ExceptionWrappingAspect.kt @@ -5,22 +5,31 @@ import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect import org.springframework.context.ApplicationContext import org.springframework.stereotype.Component +import kotlin.reflect.full.findAnnotation @Aspect @Component class ExceptionWrappingAspect( private val applicationContext: ApplicationContext, ) { - @Around("@annotation(wrapWith)") + @Around( + "execution(public * *(..)) && " + + "(@annotation(wrapWith) || @within(wrapWith))", + ) fun aroundAnnotated( joinPoint: ProceedingJoinPoint, - wrapWith: WrapWith, + wrapWith: WrapWith?, ): Any? { + val ann = + wrapWith + ?: joinPoint.signature.declaringType.kotlin + .findAnnotation() + ?: return joinPoint.proceed() + return try { joinPoint.proceed() } catch (original: Throwable) { - val strategyClass = wrapWith.value.java - val strategy = applicationContext.getBean(strategyClass) + val strategy = applicationContext.getBean(ann.value.java) throw strategy.wrap(original) } } From 054080b63a22f2dc0f45a0d8bb8c69ee905205d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 13:17:03 +0900 Subject: [PATCH 45/70] =?UTF-8?q?refactor:=20DB=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ixfp/gitmon/db/exception/DbExceptions.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt new file mode 100644 index 0000000..f76719a --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt @@ -0,0 +1,28 @@ +package com.ixfp.gitmon.db.exception + +import com.ixfp.gitmon.common.exception.AppException + +sealed class DbException( + message: String, + cause: Throwable? = null, +) : AppException(message, cause) + +class DbConnectionException( + message: String = "데이터베이스 연결에 실패했습니다.", + cause: Throwable? = null, +) : DbException(message, cause) + +class DbQueryExecutionException( + message: String = "SQL 쿼리 실행 중 예외가 발생했습니다.", + cause: Throwable? = null, +) : DbException(message, cause) + +class DbMappingException( + message: String = "DB 결과를 매핑하는 데 실패했습니다.", + cause: Throwable? = null, +) : DbException(message, cause) + +class DbUnexpectedException( + message: String = "예상치 못한 DB 관련 예외가 발생했습니다.", + cause: Throwable? = null, +) : DbException(message, cause) From 59f3da318dc6887b01360fa69e21096d8fed1198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 13:17:56 +0900 Subject: [PATCH 46/70] =?UTF-8?q?refactor:=20DB=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=9E=98=ED=95=91=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/exception/DbExceptionStrategy.kt | 23 +++++++++++++++++++ .../ixfp/gitmon/db/exception/DbExceptions.kt | 4 ++-- .../ixfp/gitmon/domain/member/MemberReader.kt | 3 +++ .../ixfp/gitmon/domain/member/MemberWriter.kt | 3 +++ .../gitmon/domain/posting/PostingReader.kt | 3 +++ .../gitmon/domain/posting/PostingWriter.kt | 3 +++ 6 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt new file mode 100644 index 0000000..ad57b8b --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt @@ -0,0 +1,23 @@ +package com.ixfp.gitmon.db.exception + +import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import com.ixfp.gitmon.common.exception.AppException +import jakarta.persistence.PersistenceException +import org.springframework.dao.DataAccessResourceFailureException +import org.springframework.stereotype.Component +import java.sql.SQLException +import org.springframework.dao.DataAccessException as SpringDataAccessException + +@Component +class DbExceptionStrategy : ExceptionWrappingStrategy { + override fun wrap(e: Throwable): AppException = + when (e) { + is AppException -> e + is DataAccessResourceFailureException, + is SQLException, + -> DbConnectionException(cause = e) + is SpringDataAccessException -> DbQueryExecutionException(cause = e) + is PersistenceException -> DbMappingException(cause = e) + else -> DbUnexpectedException(cause = e) + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt index f76719a..3785839 100644 --- a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt +++ b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptions.kt @@ -1,11 +1,11 @@ package com.ixfp.gitmon.db.exception -import com.ixfp.gitmon.common.exception.AppException +import com.ixfp.gitmon.common.exception.RepositoryException sealed class DbException( message: String, cause: Throwable? = null, -) : AppException(message, cause) +) : RepositoryException(message, cause) class DbConnectionException( message: String = "데이터베이스 연결에 실패했습니다.", diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt index 75b957d..d0a685b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberReader.kt @@ -1,10 +1,13 @@ package com.ixfp.gitmon.domain.member +import com.ixfp.gitmon.common.aop.WrapWith +import com.ixfp.gitmon.db.exception.DbExceptionStrategy import com.ixfp.gitmon.db.github.GithubAuthRepository import com.ixfp.gitmon.db.member.MemberEntity import com.ixfp.gitmon.db.member.MemberRepository import org.springframework.stereotype.Component +@WrapWith(DbExceptionStrategy::class) @Component class MemberReader( private val memberRepository: MemberRepository, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberWriter.kt index b2eb782..c09d3e9 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberWriter.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberWriter.kt @@ -1,5 +1,7 @@ package com.ixfp.gitmon.domain.member +import com.ixfp.gitmon.common.aop.WrapWith +import com.ixfp.gitmon.db.exception.DbExceptionStrategy import com.ixfp.gitmon.db.github.GithubAuthEntity import com.ixfp.gitmon.db.github.GithubAuthRepository import com.ixfp.gitmon.db.member.MemberEntity @@ -7,6 +9,7 @@ import com.ixfp.gitmon.db.member.MemberRepository import jakarta.transaction.Transactional import org.springframework.stereotype.Component +@WrapWith(DbExceptionStrategy::class) @Component class MemberWriter( private val memberRepository: MemberRepository, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt index f7cb1bc..cda65a7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt @@ -1,9 +1,12 @@ package com.ixfp.gitmon.domain.posting +import com.ixfp.gitmon.common.aop.WrapWith +import com.ixfp.gitmon.db.exception.DbExceptionStrategy import com.ixfp.gitmon.db.posting.PostingEntity import com.ixfp.gitmon.db.posting.PostingRepository import org.springframework.stereotype.Component +@WrapWith(DbExceptionStrategy::class) @Component class PostingReader( private val postingRepository: PostingRepository, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt index 099c329..91424a4 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt @@ -1,9 +1,12 @@ package com.ixfp.gitmon.domain.posting +import com.ixfp.gitmon.common.aop.WrapWith +import com.ixfp.gitmon.db.exception.DbExceptionStrategy import com.ixfp.gitmon.db.posting.PostingEntity import com.ixfp.gitmon.db.posting.PostingRepository import org.springframework.stereotype.Component +@WrapWith(DbExceptionStrategy::class) @Component class PostingWriter( private val postingRepository: PostingRepository, From 95ca8ee5ae2c717a396d28e0c184abadfd1a6ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 13:36:26 +0900 Subject: [PATCH 47/70] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8(A?= =?UTF-8?q?uth,=20Member,=20Posting)=20=EA=B3=84=EC=B8=B5=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=9E=98=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth, Member, Posting 도메인에 대해 예외 추가 - 예상하지 못한 예외만 추가 --- .../com/ixfp/gitmon/domain/auth/AuthService.kt | 3 +++ .../auth/exception/AuthExceptionStrategy.kt | 13 +++++++++++++ .../domain/auth/exception/AuthExceptions.kt | 13 +++++++++++++ .../ixfp/gitmon/domain/member/MemberService.kt | 3 +++ .../member/exception/MemberExceptionStrategy.kt | 15 +++++++++++++++ .../domain/member/exception/MemberExceptions.kt | 13 +++++++++++++ .../ixfp/gitmon/domain/posting/PostingService.kt | 3 +++ .../posting/exception/PostingExceptionStrategy.kt | 15 +++++++++++++++ .../domain/posting/exception/PostingExceptions.kt | 13 +++++++++++++ 9 files changed, 91 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptions.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt index 6f77e69..ac6615d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt @@ -1,7 +1,9 @@ package com.ixfp.gitmon.domain.auth import com.ixfp.gitmon.client.github.response.GithubUserResponse +import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.common.util.JwtUtil +import com.ixfp.gitmon.domain.auth.exception.AuthExceptionStrategy import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberReader import com.ixfp.gitmon.domain.member.MemberWriter @@ -9,6 +11,7 @@ import jakarta.transaction.Transactional import org.springframework.stereotype.Service import java.util.UUID +@WrapWith(AuthExceptionStrategy::class) @Service class AuthService( private val jwtUtil: JwtUtil, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt new file mode 100644 index 0000000..50db470 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt @@ -0,0 +1,13 @@ +package com.ixfp.gitmon.domain.auth.exception + +import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import com.ixfp.gitmon.common.exception.AppException + +class AuthExceptionStrategy : ExceptionWrappingStrategy { + override fun wrap(e: Throwable): AppException { + return when (e) { + is AppException -> e + else -> AuthUnexpectedException(cause = e) + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptions.kt new file mode 100644 index 0000000..c00165d --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptions.kt @@ -0,0 +1,13 @@ +package com.ixfp.gitmon.domain.auth.exception + +import com.ixfp.gitmon.common.exception.DomainException + +sealed class AuthException( + message: String, + cause: Throwable? = null, +) : DomainException(message, cause) + +class AuthUnexpectedException( + message: String = "예상치 못한 인증 도메인 예외가 발생했습니다.", + cause: Throwable? = null, +) : AuthException(message, cause) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index c8b07a4..72464c6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -2,9 +2,12 @@ package com.ixfp.gitmon.domain.member import com.ixfp.gitmon.client.github.GithubResourceApiClient import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest +import com.ixfp.gitmon.common.aop.WrapWith +import com.ixfp.gitmon.domain.member.exception.MemberExceptionStrategy import feign.FeignException import org.springframework.stereotype.Service +@WrapWith(MemberExceptionStrategy::class) @Service class MemberService( private val githubResourceApiClient: GithubResourceApiClient, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt new file mode 100644 index 0000000..5ee3fa9 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt @@ -0,0 +1,15 @@ +package com.ixfp.gitmon.domain.member.exception + +import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import com.ixfp.gitmon.common.exception.AppException +import org.springframework.stereotype.Component + +@Component +class MemberExceptionStrategy : ExceptionWrappingStrategy { + override fun wrap(e: Throwable): AppException { + return when (e) { + is AppException -> e + else -> MemberUnexpectedException(cause = e) + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt new file mode 100644 index 0000000..5526cc8 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt @@ -0,0 +1,13 @@ +package com.ixfp.gitmon.domain.member.exception + +import com.ixfp.gitmon.common.exception.DomainException + +sealed class MemberException( + message: String, + cause: Throwable? = null, +) : DomainException(message, cause) + +class MemberUnexpectedException( + message: String = "예상치 못한 회원 도메인 예외가 발생했습니다.", + cause: Throwable? = null, +) : MemberException(message, cause) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 8a3c078..e231a34 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -1,13 +1,16 @@ package com.ixfp.gitmon.domain.posting import com.ixfp.gitmon.client.github.GithubApiService +import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberReader +import com.ixfp.gitmon.domain.posting.exception.PostingExceptionStrategy import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import java.util.UUID +@WrapWith(PostingExceptionStrategy::class) @Service class PostingService( private val memberReader: MemberReader, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt new file mode 100644 index 0000000..c5e4c31 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt @@ -0,0 +1,15 @@ +package com.ixfp.gitmon.domain.posting.exception + +import com.ixfp.gitmon.common.aop.ExceptionWrappingStrategy +import com.ixfp.gitmon.common.exception.AppException +import org.springframework.stereotype.Component + +@Component +class PostingExceptionStrategy : ExceptionWrappingStrategy { + override fun wrap(e: Throwable): AppException { + return when (e) { + is AppException -> e + else -> PostingUnexpectedException(cause = e) + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt new file mode 100644 index 0000000..8aecacd --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt @@ -0,0 +1,13 @@ +package com.ixfp.gitmon.domain.posting.exception + +import com.ixfp.gitmon.common.exception.DomainException + +sealed class PostingException( + message: String, + cause: Throwable? = null, +) : DomainException(message, cause) + +class PostingUnexpectedException( + message: String = "예상치 못한 게시글 도메인 예외가 발생했습니다.", + cause: Throwable? = null, +) : PostingException(message, cause) From b0677321b9d84164fc7cb4194210543ff01ac85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 13:54:57 +0900 Subject: [PATCH 48/70] =?UTF-8?q?refactor:=20Exception=20Strategy=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=B5=EA=B3=BC=ED=95=98=EB=8A=94=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A5=BC=20=EA=B4=80=EC=8B=AC=EC=82=AC=EC=9D=98=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A1=9C=20=ED=95=9C=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt | 2 +- .../ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt | 2 +- .../gitmon/domain/member/exception/MemberExceptionStrategy.kt | 2 +- .../gitmon/domain/posting/exception/PostingExceptionStrategy.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt index ad57b8b..3154311 100644 --- a/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/db/exception/DbExceptionStrategy.kt @@ -12,7 +12,7 @@ import org.springframework.dao.DataAccessException as SpringDataAccessException class DbExceptionStrategy : ExceptionWrappingStrategy { override fun wrap(e: Throwable): AppException = when (e) { - is AppException -> e + is DbException -> e is DataAccessResourceFailureException, is SQLException, -> DbConnectionException(cause = e) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt index 50db470..cb5e4b9 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/auth/exception/AuthExceptionStrategy.kt @@ -6,7 +6,7 @@ import com.ixfp.gitmon.common.exception.AppException class AuthExceptionStrategy : ExceptionWrappingStrategy { override fun wrap(e: Throwable): AppException { return when (e) { - is AppException -> e + is AuthException -> e else -> AuthUnexpectedException(cause = e) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt index 5ee3fa9..b4bacba 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptionStrategy.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component class MemberExceptionStrategy : ExceptionWrappingStrategy { override fun wrap(e: Throwable): AppException { return when (e) { - is AppException -> e + is MemberException -> e else -> MemberUnexpectedException(cause = e) } } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt index c5e4c31..55c276b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptionStrategy.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component class PostingExceptionStrategy : ExceptionWrappingStrategy { override fun wrap(e: Throwable): AppException { return when (e) { - is AppException -> e + is PostingException -> e else -> PostingUnexpectedException(cause = e) } } From 5daf525b8f589b621f2b8938b40f22bc5d65a1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 14:01:20 +0900 Subject: [PATCH 49/70] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20"=EC=97=91=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=97=86=EC=9D=8C"=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ixfp/gitmon/domain/member/MemberService.kt | 5 ++++- .../ixfp/gitmon/domain/member/exception/MemberExceptions.kt | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index 72464c6..2169e1e 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -4,6 +4,7 @@ import com.ixfp.gitmon.client.github.GithubResourceApiClient import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.exception.MemberExceptionStrategy +import com.ixfp.gitmon.domain.member.exception.MemberGithubAccessTokenNotFoundException import feign.FeignException import org.springframework.stereotype.Service @@ -62,7 +63,9 @@ class MemberService( member: Member, repoName: String, ): Boolean { - val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) ?: throw Error("Github 엑세스 토큰 없음") + val githubAccessToken = + memberReader.findAccessTokenByMemberId(member.id) + ?: throw MemberGithubAccessTokenNotFoundException() return try { githubResourceApiClient.fetchRepository( token = "token $githubAccessToken", diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt index 5526cc8..ffc04d6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt @@ -11,3 +11,8 @@ class MemberUnexpectedException( message: String = "예상치 못한 회원 도메인 예외가 발생했습니다.", cause: Throwable? = null, ) : MemberException(message, cause) + +class MemberGithubAccessTokenNotFoundException( + message: String = "해당 회원의 GitHub 액세스 토큰이 존재하지 않습니다.", + cause: Throwable? = null, +) : MemberException(message, cause) From 3aa3925d37cdcac56a5c3d417bb725a2968d96f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 14:13:12 +0900 Subject: [PATCH 50/70] =?UTF-8?q?refactor:=20Member=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EA=B0=80=20=EB=9E=98=ED=95=91=EB=90=9C=20Github=20Api?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/client/github/GithubApiService.kt | 29 +++++++++++++++++++ .../gitmon/domain/member/MemberService.kt | 24 +++++---------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 3042e2b..3c493a3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -3,6 +3,7 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.exception.GithubApiExceptionStrategy import com.ixfp.gitmon.client.github.exception.GithubInvalidAuthCodeException import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest +import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubContent import com.ixfp.gitmon.client.github.response.GithubUserResponse @@ -10,6 +11,7 @@ import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken +import feign.FeignException import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile @@ -76,6 +78,33 @@ class GithubApiService( return response.content } + fun createRepository( + accessToken: String, + request: GithubCreateRepositoryRequest, + ) { + githubResourceApiClient.createRepository( + bearerToken = "Bearer $accessToken", + request = request, + ) + } + + fun isRepositoryExist( + token: String, + owner: String, + repo: String, + ): Boolean { + return try { + githubResourceApiClient.fetchRepository( + token = token, + owner = owner, + repo = repo, + ) + true + } catch (e: FeignException.NotFound) { + false + } + } + private fun buildAuthRedirectionUrl(githubClientId: String): String { return "https://github.com/login/oauth/authorize?&scope=repo&client_id=$githubClientId" } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index 2169e1e..a7f0e63 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -1,17 +1,16 @@ package com.ixfp.gitmon.domain.member -import com.ixfp.gitmon.client.github.GithubResourceApiClient +import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.exception.MemberExceptionStrategy import com.ixfp.gitmon.domain.member.exception.MemberGithubAccessTokenNotFoundException -import feign.FeignException import org.springframework.stereotype.Service @WrapWith(MemberExceptionStrategy::class) @Service class MemberService( - private val githubResourceApiClient: GithubResourceApiClient, + private val githubApiService: GithubApiService, private val memberWriter: MemberWriter, private val memberReader: MemberReader, ) { @@ -28,7 +27,7 @@ class MemberService( private = false, ) // TODO: DB 레포지토리 업데이트 오류 시 생성된 레포지토리를 지워야 함 - githubResourceApiClient.createRepository("Bearer $githubAccessToken", githubRequest) + githubApiService.createRepository(githubAccessToken, githubRequest) memberWriter.upsertRepo(member, repoName) } @@ -66,17 +65,10 @@ class MemberService( val githubAccessToken = memberReader.findAccessTokenByMemberId(member.id) ?: throw MemberGithubAccessTokenNotFoundException() - return try { - githubResourceApiClient.fetchRepository( - token = "token $githubAccessToken", - owner = member.githubUsername, - repo = repoName, - ) - true - } catch (e: FeignException.NotFound) { - false - } catch (e: Exception) { - throw RuntimeException("GitHub API 요청 실패: ${e.message}") - } + return githubApiService.isRepositoryExist( + token = "token $githubAccessToken", + owner = member.githubUsername, + repo = repoName, + ) } } From 84f45e577bfa953b9a2298344e19d51ea31cd02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Mon, 5 May 2025 14:40:02 +0900 Subject: [PATCH 51/70] =?UTF-8?q?refactor:=20=ED=8F=AC=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=EA=B0=80=20=EC=9E=90=EC=B2=B4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 포스팅 서비스가 자체 예외를 사용하도록 리팩토링 - 회원 리더를 직접 사용하지 않고, 회원 서비스를 사용하도록 변경 --- .../gitmon/domain/member/MemberService.kt | 15 ++++++++--- .../member/exception/MemberExceptions.kt | 5 ++++ .../gitmon/domain/posting/PostingService.kt | 25 ++++++++----------- .../posting/exception/PostingExceptions.kt | 10 ++++++++ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index a7f0e63..7d78ac6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -5,6 +5,7 @@ import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.exception.MemberExceptionStrategy import com.ixfp.gitmon.domain.member.exception.MemberGithubAccessTokenNotFoundException +import com.ixfp.gitmon.domain.member.exception.MemberNotFoundException import org.springframework.stereotype.Service @WrapWith(MemberExceptionStrategy::class) @@ -14,6 +15,16 @@ class MemberService( private val memberWriter: MemberWriter, private val memberReader: MemberReader, ) { + fun getMemberByExposedId(exposedId: String): Member { + return memberReader.findByExposedId(exposedId) + ?: throw MemberNotFoundException(message = "exposedId: $exposedId") + } + + fun getGithubAccessTokenById(id: Long): String { + return memberReader.findAccessTokenByMemberId(id) + ?: throw MemberGithubAccessTokenNotFoundException(message = "memberId: $id") + } + fun upsertRepo( member: Member, repoName: String, @@ -62,9 +73,7 @@ class MemberService( member: Member, repoName: String, ): Boolean { - val githubAccessToken = - memberReader.findAccessTokenByMemberId(member.id) - ?: throw MemberGithubAccessTokenNotFoundException() + val githubAccessToken = getGithubAccessTokenById(member.id) return githubApiService.isRepositoryExist( token = "token $githubAccessToken", owner = member.githubUsername, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt index ffc04d6..58441df 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/exception/MemberExceptions.kt @@ -12,6 +12,11 @@ class MemberUnexpectedException( cause: Throwable? = null, ) : MemberException(message, cause) +class MemberNotFoundException( + message: String = "해당 회원을 찾을 수 없습니다.", + cause: Throwable? = null, +) : MemberException(message, cause) + class MemberGithubAccessTokenNotFoundException( message: String = "해당 회원의 GitHub 액세스 토큰이 존재하지 않습니다.", cause: Throwable? = null, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index e231a34..d05b24d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -3,8 +3,10 @@ package com.ixfp.gitmon.domain.posting import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.Member -import com.ixfp.gitmon.domain.member.MemberReader +import com.ixfp.gitmon.domain.member.MemberService +import com.ixfp.gitmon.domain.posting.exception.InvalidImageExtensionException import com.ixfp.gitmon.domain.posting.exception.PostingExceptionStrategy +import com.ixfp.gitmon.domain.posting.exception.PostingRepositoryNotFoundException import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile @@ -13,15 +15,13 @@ import java.util.UUID @WrapWith(PostingExceptionStrategy::class) @Service class PostingService( - private val memberReader: MemberReader, + private val memberService: MemberService, private val githubApiService: GithubApiService, private val postingReader: PostingReader, private val postingWriter: PostingWriter, ) { fun findPostingListByMemberExposedId(memberExposedId: String): List { - val member = - memberReader.findByExposedId(memberExposedId) - ?: throw IllegalArgumentException("존재하지 않는 회원입니다.") + val member = memberService.getMemberByExposedId(memberExposedId) return postingReader.findPostingListByMemberId(member.id) } @@ -31,12 +31,10 @@ class PostingService( content: MultipartFile, ) { log.info { "PostingService#create start. member=$member, title=$title, content.size=${content.size}" } - val githubAccessToken = - memberReader.findAccessTokenByMemberId(member.id) - ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") + val githubAccessToken = memberService.getGithubAccessTokenById(member.id) if (member.repoName == null) { - throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") + throw PostingRepositoryNotFoundException() } val githubContent = @@ -65,15 +63,12 @@ class PostingService( member: Member, content: MultipartFile, ): String { - val githubAccessToken = - memberReader.findAccessTokenByMemberId(member.id) - ?: throw IllegalArgumentException("Github 엑세스 토큰이 없습니다.") - if (member.repoName == null) { - throw IllegalArgumentException("포스팅 레포지토리가 없습니다.") + throw PostingRepositoryNotFoundException() } - val extension = resolveExtension(content) ?: throw IllegalArgumentException("파일의 확장자가 이미지가 아닙니다.") + val githubAccessToken = memberService.getGithubAccessTokenById(member.id) + val extension = resolveExtension(content) ?: throw InvalidImageExtensionException() val imageId = UUID.randomUUID().toString() val filename = "$imageId.$extension" val path = "images/$filename" diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt index 8aecacd..3c60937 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt @@ -7,6 +7,16 @@ sealed class PostingException( cause: Throwable? = null, ) : DomainException(message, cause) +class PostingRepositoryNotFoundException( + message: String = "회원의 게시글 레포지토리가 없습니다.", + cause: Throwable? = null, +) : PostingException(message, cause) + +class InvalidImageExtensionException( + message: String = "업로드하는 파일의 확장자가 이미지가 아닙니다.", + cause: Throwable? = null, +) : PostingException(message, cause) + class PostingUnexpectedException( message: String = "예상치 못한 게시글 도메인 예외가 발생했습니다.", cause: Throwable? = null, From 7e48966e374d6f9f2a7e46af1e6ebf9e9253d5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 6 May 2025 12:14:36 +0900 Subject: [PATCH 52/70] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80,=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 응답 메시지에 응답코드를 포함해 반환하도록 변경 - API 응답 헬퍼 구현 및 적용 - API Response Body, API Error 파일 분리 및 컨트롤러 계층으로 위치 변경 --- .../gitmon/common/type/ApiResponseBody.kt | 45 ---------- .../gitmon/controller/IMemberController.kt | 10 ++- .../gitmon/controller/IOauth2Controller.kt | 3 +- .../gitmon/controller/IPostingController.kt | 7 +- .../gitmon/controller/MemberController.kt | 85 ++++++------------- .../gitmon/controller/Oauth2Controller.kt | 61 +++++-------- .../gitmon/controller/PostingController.kt | 39 +++------ .../ixfp/gitmon/controller/type/ApiError.kt | 12 +++ .../gitmon/controller/type/ApiResponseBody.kt | 42 +++++++++ .../controller/util/ApiResponseHelper.kt | 46 ++++++++++ 10 files changed, 172 insertions(+), 178 deletions(-) delete mode 100644 src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/type/ApiResponseBody.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt b/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt deleted file mode 100644 index 17f8da3..0000000 --- a/src/main/kotlin/com/ixfp/gitmon/common/type/ApiResponseBody.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ixfp.gitmon.common.type - -data class ApiResponseBody( - val status: ApiStatus, - val data: T? = null, - val error: ApiErrorType? = null, -) { - companion object { - fun success(): ApiResponseBody { - return ApiResponseBody( - status = ApiStatus.SUCCESS, - ) - } - - fun success(data: T): ApiResponseBody { - return ApiResponseBody( - status = ApiStatus.SUCCESS, - data = data, - ) - } - - fun error(error: ApiErrorType): ApiResponseBody { - return ApiResponseBody( - status = error.status, - error = error, - ) - } - } -} - -enum class ApiStatus { - SUCCESS, - UNAUTHORIZED, - FORBIDDEN, - ERROR, -} - -enum class ApiErrorType(val status: ApiStatus, message: String) { - BAD_REQUEST(ApiStatus.ERROR, "Bad request"), - UNAUTHORIZED(ApiStatus.UNAUTHORIZED, "Unauthorized"), - INVALID_ACCESS_TOKEN(ApiStatus.UNAUTHORIZED, "Invalid access token"), - EXPIRED_ACCESS_TOKEN(ApiStatus.UNAUTHORIZED, "Access token expired"), - - INTERNAL_SERVER_ERROR(ApiStatus.ERROR, "Internal server error"), -} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index 6b3f7f6..61e5644 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -5,6 +5,7 @@ import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.controller.response.MemberInfoResponse +import com.ixfp.gitmon.controller.type.ApiResponseBody import com.ixfp.gitmon.domain.member.Member import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content @@ -13,6 +14,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity @Tag(name = "Member", description = "회원 관련 API") interface IMemberController { @@ -46,7 +48,7 @@ interface IMemberController { ), ], ) - fun getMember(member: Member): com.ixfp.gitmon.common.type.ApiResponseBody + fun getMember(member: Member): ResponseEntity> @Operation( summary = "레포지토리 생성/갱신", @@ -78,7 +80,7 @@ interface IMemberController { fun upsertRepo( request: CreateRepoRequest, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponseBody + ): ResponseEntity> @Operation( summary = "레포지토리 이름 중복 조회", @@ -112,7 +114,7 @@ interface IMemberController { fun checkRepoName( name: String, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponseBody + ): ResponseEntity> @Operation(summary = "레포지토리 URL 조회") @ApiResponses( @@ -139,5 +141,5 @@ interface IMemberController { ), ], ) - fun findGithubRepoUrl(githubUsername: String): com.ixfp.gitmon.common.type.ApiResponseBody + fun findGithubRepoUrl(githubUsername: String): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index 5694829..7817d8d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -2,6 +2,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse +import com.ixfp.gitmon.controller.type.ApiResponseBody import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content @@ -63,5 +64,5 @@ interface IOauth2Controller { fun login( profile: String?, request: GithubOauth2Request, - ): com.ixfp.gitmon.common.type.ApiResponseBody + ): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index cd8d80b..0c3e015 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -1,7 +1,9 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.config.docs.ACCESS_TOKEN +import com.ixfp.gitmon.controller.type.ApiResponseBody import com.ixfp.gitmon.domain.member.Member +import com.ixfp.gitmon.domain.posting.PostingReadDto import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -9,6 +11,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity import org.springframework.web.multipart.MultipartFile @Tag(name = "Posting", description = "게시글(포스팅) 관련 API") @@ -44,5 +47,7 @@ interface IPostingController { title: String, content: MultipartFile, member: Member, - ): com.ixfp.gitmon.common.type.ApiResponseBody + ): ResponseEntity> + + fun getPostingList(exposedMemberId: String): ResponseEntity>> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index d175be6..fb81273 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,16 +1,17 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.controller.response.CheckRepoNameResponse import com.ixfp.gitmon.controller.response.GithubRepoUrlResponse import com.ixfp.gitmon.controller.response.MemberInfoResponse +import com.ixfp.gitmon.controller.type.ApiResponseBody +import com.ixfp.gitmon.controller.util.ApiResponseHelper import com.ixfp.gitmon.domain.auth.AuthService import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import io.github.oshai.kotlinlogging.KotlinLogging.logger +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestAttribute @@ -29,86 +30,52 @@ class MemberController( @GetMapping override fun getMember( @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponseBody { - return try { - val response = - MemberInfoResponse( - id = member.exposedId, - githubUsername = member.githubUsername, - repoName = member.repoName, - isRepoCreated = member.repoName != null, - ) - ApiResponseBody.success(response) - } catch (e: Exception) { - log.error(e) { "Failed to get member: ${e.message}" } - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - is AuthenticationException -> ApiErrorType.UNAUTHORIZED - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - ApiResponseBody.error(errorType) - } + ): ResponseEntity> { + val response = + MemberInfoResponse( + id = member.exposedId, + githubUsername = member.githubUsername, + repoName = member.repoName, + isRepoCreated = member.repoName != null, + ) + return ApiResponseHelper.success(response) } @PostMapping("/repo") override fun upsertRepo( @RequestBody request: CreateRepoRequest, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponseBody { - try { - val githubAccessToken = - authService.getGithubAccessToken(member.id) - ?: throw AuthenticationException("사용자의 깃허브 토큰을 찾을 수 없음") + ): ResponseEntity> { + val githubAccessToken = + authService.getGithubAccessToken(member.id) + ?: throw AuthenticationException("사용자의 깃허브 토큰을 찾을 수 없음") - if (member.repoName == request.name) { - return ApiResponseBody.success() - } - memberService.upsertRepo(member, request.name, githubAccessToken) - return ApiResponseBody.success() - } catch (e: Exception) { - log.error(e) { "Failed to create repository: ${e.message}" } - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - is AuthenticationException -> ApiErrorType.UNAUTHORIZED - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - return ApiResponseBody.error(errorType) + if (member.repoName == request.name) { + return ApiResponseHelper.success() } + memberService.upsertRepo(member, request.name, githubAccessToken) + return ApiResponseHelper.success() } @GetMapping("/repo/check") override fun checkRepoName( @RequestParam name: String, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponseBody { - return try { - val isAvailable = memberService.isRepoNameAvailable(member, name) - ApiResponseBody.success(CheckRepoNameResponse(isAvailable)) - } catch (e: Exception) { - log.error(e) { "Failed to check repository name: ${e.message}" } - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - is AuthenticationException -> ApiErrorType.UNAUTHORIZED - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - ApiResponseBody.error(errorType) - } + ): ResponseEntity> { + val isAvailable = memberService.isRepoNameAvailable(member, name) + return ApiResponseHelper.success(CheckRepoNameResponse(isAvailable)) } @GetMapping("/github/repo") override fun findGithubRepoUrl( @RequestParam githubUsername: String, - ): ApiResponseBody { + ): ResponseEntity> { val githubRepoUrl = memberService.findGithubRepoUrl(githubUsername) - ?: return ApiResponseBody.success() + ?: return ApiResponseHelper.success() val response = GithubRepoUrlResponse(githubRepoUrl) - - return ApiResponseBody.success(response) + return ApiResponseHelper.success(response) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 31b1b39..59a64ad 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -1,14 +1,13 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.client.github.GithubApiService -import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse +import com.ixfp.gitmon.controller.type.ApiResponseBody +import com.ixfp.gitmon.controller.util.ApiResponseHelper import com.ixfp.gitmon.domain.auth.AuthService import io.github.oshai.kotlinlogging.KotlinLogging.logger -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -16,7 +15,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import javax.naming.AuthenticationException @RestController @RequestMapping("/api/v1") @@ -27,47 +25,32 @@ class Oauth2Controller( @GetMapping("/login/oauth/github") override fun redirectToGithubOauthUrl(profile: String?): ResponseEntity { val redirectionUrl = githubApiService.getAuthRedirectionUrl(Profile.findByCode(profile)) - - return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header( - "Location", - redirectionUrl, - ).build() + return ApiResponseHelper.redirectPermanent(redirectionUrl) } @PostMapping("/login/oauth/github/tokens") override fun login( @RequestParam profile: String?, @RequestBody request: GithubOauth2Request, - ): ApiResponseBody { - try { - val githubAccessToken = githubApiService.getAccessTokenByCode(request.code, Profile.findByCode(profile)) - val githubUser = githubApiService.getGithubUser(githubAccessToken) - val member = - authService.getMemberByGithubId(githubUser.id.toLong()) - ?: authService.signup(githubAccessToken, githubUser) - - authService.login(githubAccessToken, member) - - val accessToken = authService.createAccessToken(member) - val isRepoCreated = member.repoName != null - - val response = - AccessTokenResponse( - id = member.exposedId, - accessToken = accessToken, - isRepoCreated = isRepoCreated, - ) - return ApiResponseBody.success(response) - } catch (e: Exception) { - log.error(e) { "Failed to login: ${e.message}" } - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - is AuthenticationException -> ApiErrorType.UNAUTHORIZED - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - return ApiResponseBody.error(errorType) - } + ): ResponseEntity> { + val githubAccessToken = githubApiService.getAccessTokenByCode(request.code, Profile.findByCode(profile)) + val githubUser = githubApiService.getGithubUser(githubAccessToken) + val member = + authService.getMemberByGithubId(githubUser.id.toLong()) + ?: authService.signup(githubAccessToken, githubUser) + + authService.login(githubAccessToken, member) + + val accessToken = authService.createAccessToken(member) + val isRepoCreated = member.repoName != null + + val response = + AccessTokenResponse( + id = member.exposedId, + accessToken = accessToken, + isRepoCreated = isRepoCreated, + ) + return ApiResponseHelper.success(response) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 7c82255..77d17ab 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -1,13 +1,14 @@ package com.ixfp.gitmon.controller -import com.ixfp.gitmon.common.type.ApiErrorType -import com.ixfp.gitmon.common.type.ApiResponseBody import com.ixfp.gitmon.config.web.AUTHENTICATED_MEMBER +import com.ixfp.gitmon.controller.type.ApiResponseBody +import com.ixfp.gitmon.controller.util.ApiResponseHelper import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.posting.PostingReadDto import com.ixfp.gitmon.domain.posting.PostingService import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -30,37 +31,17 @@ class PostingController( @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ApiResponseBody { - try { - postingService.create(member, title, content) - return ApiResponseBody.success() - } catch (e: Exception) { - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - log.error { "포스팅 생성 중 에러 발생: ${e.message}" } - return ApiResponseBody.error(errorType) - } + ): ResponseEntity> { + postingService.create(member, title, content) + return ApiResponseHelper.success() } @GetMapping("/{exposedMemberId}") - fun getPostingList( + override fun getPostingList( @PathVariable exposedMemberId: String, - ): ApiResponseBody> { - try { - val postingList = postingService.findPostingListByMemberExposedId(exposedMemberId) - return ApiResponseBody.success(postingList) - } catch (e: Exception) { - val errorType = - when (e) { - is IllegalArgumentException -> ApiErrorType.BAD_REQUEST - else -> ApiErrorType.INTERNAL_SERVER_ERROR - } - log.error { "포스팅 목록 조회 중 에러 발생: ${e.message}" } - return ApiResponseBody.error(errorType) - } + ): ResponseEntity>> { + val postingList = postingService.findPostingListByMemberExposedId(exposedMemberId) + return ApiResponseHelper.success(postingList) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt new file mode 100644 index 0000000..a8e73ea --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt @@ -0,0 +1,12 @@ +package com.ixfp.gitmon.controller.type + +import org.springframework.http.HttpStatus + +enum class ApiError(val httpStatus: HttpStatus, val message: String) { + BAD_REQUEST(HttpStatus.BAD_REQUEST, "Bad request"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized"), + FORBIDDEN(HttpStatus.FORBIDDEN, "forbidden"), + INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid access token"), + EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access token expired"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiResponseBody.kt b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiResponseBody.kt new file mode 100644 index 0000000..5532ec3 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiResponseBody.kt @@ -0,0 +1,42 @@ +package com.ixfp.gitmon.controller.type + +import org.springframework.http.HttpStatus + +data class ApiResponseBody( + val status: HttpStatus, + val statusCode: Int, + val data: T? = null, + val errorMessage: String? = null, +) { + companion object { + fun success(): ApiResponseBody { + return ApiResponseBody( + status = HttpStatus.OK, + statusCode = HttpStatus.OK.value(), + ) + } + + fun success( + status: HttpStatus = HttpStatus.OK, + data: T?, + ): ApiResponseBody { + return ApiResponseBody( + status = status, + statusCode = status.value(), + data = data, + ) + } + + fun error( + error: ApiError, + data: T? = null, + ): ApiResponseBody { + return ApiResponseBody( + status = error.httpStatus, + statusCode = error.httpStatus.value(), + errorMessage = error.message, + data = data, + ) + } + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt b/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt new file mode 100644 index 0000000..d39c464 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt @@ -0,0 +1,46 @@ +package com.ixfp.gitmon.controller.util + +import com.ixfp.gitmon.controller.type.ApiError +import com.ixfp.gitmon.controller.type.ApiResponseBody +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +object ApiResponseHelper { + fun success(data: T? = null): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponseBody.success(HttpStatus.OK, data)) + } + + fun created(data: T? = null): ResponseEntity> { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponseBody.success(HttpStatus.CREATED, data)) + } + + fun redirectPermanent(location: String): ResponseEntity { + return ResponseEntity + .status(HttpStatus.MOVED_PERMANENTLY) + .header(HttpHeaders.LOCATION, location) + .build() + } + + fun custom( + status: HttpStatus, + data: T? = null, + ): ResponseEntity> { + return ResponseEntity + .status(status) + .body(ApiResponseBody.success(status, data)) + } + + fun error( + apiError: ApiError, + data: T? = null, + ): ResponseEntity> { + return ResponseEntity + .status(apiError.httpStatus) + .body(ApiResponseBody.error(apiError, data)) + } +} From 9421f738af55560ce3d238d3606175570aa4c05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 6 May 2025 13:33:08 +0900 Subject: [PATCH 53/70] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=96=B4=EB=93=9C=EB=B0=94=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EC=A0=81=EC=A0=88?= =?UTF-8?q?=ED=9E=88=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ControllerExceptionHandler.kt | 48 +++++++++++++++++++ .../ixfp/gitmon/controller/type/ApiError.kt | 3 ++ 2 files changed, 51 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/ControllerExceptionHandler.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/ControllerExceptionHandler.kt b/src/main/kotlin/com/ixfp/gitmon/controller/ControllerExceptionHandler.kt new file mode 100644 index 0000000..868633e --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/ControllerExceptionHandler.kt @@ -0,0 +1,48 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.client.github.exception.GithubInvalidAuthCodeException +import com.ixfp.gitmon.controller.type.ApiError +import com.ixfp.gitmon.controller.type.ApiResponseBody +import com.ixfp.gitmon.controller.util.ApiResponseHelper +import com.ixfp.gitmon.domain.auth.exception.AuthException +import com.ixfp.gitmon.domain.posting.exception.InvalidImageExtensionException +import com.ixfp.gitmon.domain.posting.exception.PostingRepositoryNotFoundException +import io.github.oshai.kotlinlogging.KotlinLogging.logger +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class ControllerExceptionHandler { + @ExceptionHandler(GithubInvalidAuthCodeException::class) + fun handleInvalidAuthCodeException(ex: GithubInvalidAuthCodeException): ResponseEntity> { + return ApiResponseHelper.error(ApiError.INVALID_AUTH_CODE) + } + + @ExceptionHandler(AuthException::class) + fun handleInvalidAuthException(ex: AuthException): ResponseEntity> { + // TODO: Auth 예외 구체화 + return ApiResponseHelper.error(ApiError.UNAUTHORIZED) + } + + @ExceptionHandler(InvalidImageExtensionException::class) + fun handleInvalidImageExtensionException(ex: InvalidImageExtensionException): ResponseEntity> { + return ApiResponseHelper.error(ApiError.BAD_REQUEST, "invalid image extension") + } + + @ExceptionHandler(PostingRepositoryNotFoundException::class) + fun handlePostingRepositoryNotFoundException(ex: PostingRepositoryNotFoundException): ResponseEntity> { + return ApiResponseHelper.error(ApiError.REPOSITORY_NOT_CONFIGURED) + } + + @ExceptionHandler(Exception::class) + fun handleAll(ex: Exception): ResponseEntity> { + log.warn(ex) { "예상하지 못한 예외" } + // TODO: 상황에 따라 error 레벨의 로그 추가 + return ApiResponseHelper.error(ApiError.INTERNAL_SERVER_ERROR) + } + + companion object { + private val log = logger {} + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt index a8e73ea..59d36d2 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt @@ -6,6 +6,9 @@ enum class ApiError(val httpStatus: HttpStatus, val message: String) { BAD_REQUEST(HttpStatus.BAD_REQUEST, "Bad request"), UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized"), FORBIDDEN(HttpStatus.FORBIDDEN, "forbidden"), + CONFLICT(HttpStatus.CONFLICT, "conflict"), + REPOSITORY_NOT_CONFIGURED(HttpStatus.CONFLICT, "repository not configured"), + INVALID_AUTH_CODE(HttpStatus.UNAUTHORIZED, "Invalid auth code"), INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid access token"), EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access token expired"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), From 99be576d9e2ffdca8a9ec9125c422e549bd9a3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 6 May 2025 13:58:56 +0900 Subject: [PATCH 54/70] =?UTF-8?q?chore:=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt | 2 +- src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index fb81273..6a3b19f 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -54,7 +54,7 @@ class MemberController( return ApiResponseHelper.success() } memberService.upsertRepo(member, request.name, githubAccessToken) - return ApiResponseHelper.success() + return ApiResponseHelper.created() } @GetMapping("/repo/check") diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 77d17ab..8472038 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -33,7 +33,7 @@ class PostingController( @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, ): ResponseEntity> { postingService.create(member, title, content) - return ApiResponseHelper.success() + return ApiResponseHelper.created() } @GetMapping("/{exposedMemberId}") From ace10e314b2515eb659e793b8b5bf15d044ec6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 6 May 2025 14:02:48 +0900 Subject: [PATCH 55/70] =?UTF-8?q?chore:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/controller/IMemberController.kt | 35 +++++++++--- .../gitmon/controller/IOauth2Controller.kt | 35 +++++++++--- .../gitmon/controller/IPostingController.kt | 54 +++++++++++++++++-- 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt index 61e5644..4d08636 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IMemberController.kt @@ -25,6 +25,7 @@ interface IMemberController { @ApiResponses( value = [ ApiResponse( + responseCode = "200", content = [ Content( mediaType = "application/json", @@ -58,19 +59,37 @@ interface IMemberController { @ApiResponses( value = [ ApiResponse( - description = "레포 생성/갱신 성공", + responseCode = "201", + description = "레포 생성 성공", content = [ Content( mediaType = "application/json", schema = Schema( - example = - """ - { - "status": "SUCCESS", - "data": null - } - """, + example = """ + { + "status": "CREATED", + "data": null + } + """, + ), + ), + ], + ), + ApiResponse( + responseCode = "200", + description = "레포 갱신 성공", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "SUCCESS", + "data": null + } + """, ), ), ], diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index 7817d8d..57db0e6 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -4,13 +4,13 @@ import com.ixfp.gitmon.controller.request.GithubOauth2Request import com.ixfp.gitmon.controller.response.AccessTokenResponse import com.ixfp.gitmon.controller.type.ApiResponseBody import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.headers.Header 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.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag -import org.springframework.http.MediaType +import org.springframework.http.HttpHeaders import org.springframework.http.ResponseEntity @Tag(name = "OAuth Token Control", description = "OAuth 인증 관련 API") @@ -20,11 +20,12 @@ interface IOauth2Controller { value = [ ApiResponse( responseCode = "301", - description = "Success", - content = [ - Content( - mediaType = MediaType.APPLICATION_JSON_VALUE, - array = ArraySchema(schema = Schema(implementation = String::class)), + description = "Redirect to GitHub OAuth page", + headers = [ + Header( + name = HttpHeaders.LOCATION, + description = "GitHub OAuth authorization URL", + schema = Schema(type = "string", format = "uri"), ), ], ), @@ -39,6 +40,7 @@ interface IOauth2Controller { @ApiResponses( value = [ ApiResponse( + responseCode = "200", description = "로그인 및 토큰 발급 성공", content = [ Content( @@ -58,7 +60,24 @@ interface IOauth2Controller { ), ], ), - // TODO: 깃허브 API 에러로 인한 에러 응답 예제 추가 + ApiResponse( + responseCode = "401", + description = "유효하지 않은 GitHub 인증 코드", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "UNAUTHORIZED", + "errorMessage": "Invalid auth code" + } + """, + ), + ), + ], + ), ], ) fun login( diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 0c3e015..5eb67b7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.multipart.MultipartFile @@ -24,23 +25,68 @@ interface IPostingController { @ApiResponses( value = [ ApiResponse( + responseCode = "201", description = "포스팅 생성 성공", content = [ Content( mediaType = "application/json", schema = Schema( - example = - """ + example = """ { - "status": "SUCCESS", + "status": "CREATED", "data": null } - """, + """, ), ), ], ), + ApiResponse( + responseCode = "400", + description = "허용되지 않은 이미지 확장자", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "BAD_REQUEST", + "errorMessage": "invalid image extension" + } + """, + ), + ), + ], + ), + ApiResponse( + responseCode = "409", + description = "사용자의 레포지토리가 아직 설정되지 않음", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "CONFLICT", + "errorMessage": "repository not configured" + } + """, + ), + ), + ], + ), + ], + ) + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "포스팅 생성 요청 (제목 + 파일)", + required = true, + content = [ + Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + ), ], ) fun createPosting( From cf5ea67e013b06d64f28d73df9e626011e5ebc04 Mon Sep 17 00:00:00 2001 From: TraceofLight Date: Sun, 11 May 2025 18:18:51 +0900 Subject: [PATCH 56/70] refactor: remove test file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api 테스트 관련 파일 제거 - 만료 토큰 등의 정보가 포함된 api 테스트 파일 제거 --- http/github-oauth2.http | 10 ---------- http/member-repo.http | 12 ------------ 2 files changed, 22 deletions(-) delete mode 100644 http/github-oauth2.http delete mode 100644 http/member-repo.http diff --git a/http/github-oauth2.http b/http/github-oauth2.http deleted file mode 100644 index 5c4801f..0000000 --- a/http/github-oauth2.http +++ /dev/null @@ -1,10 +0,0 @@ -### 깃허브 인증 페이지 리다이렉트 -GET http://localhost:8080/api/v1/login/oauth/github - -### 깃허브 인증 코드로 github access token 발급 -POST http://localhost:8080/api/v1/login/oauth/github/tokens -Content-Type: application/json - -{ - "code": "3ad1cb50aa501f66faca" -} diff --git a/http/member-repo.http b/http/member-repo.http deleted file mode 100644 index 8c5e3fb..0000000 --- a/http/member-repo.http +++ /dev/null @@ -1,12 +0,0 @@ -### 사용자 repo 생성 -POST http://localhost:8080/api/v1/member/repo -Content-Type: application/json -Authorization: Bearer jwt - -{ - "name": "gitmon" -} - -### 사용자 repo 이름 중복 확인 -GET http://localhost:8080/api/v1/member/repo/check?name=gitmon -Authorization: Bearer jwt From 15db6f465bea4dac3dcd506a56855867d2be7f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 27 May 2025 16:45:35 +0900 Subject: [PATCH 57/70] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20refresh=20token=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ixfp/gitmon/common/util/JwtUtil.kt | 18 +++++++++-- .../gitmon/controller/IOauth2Controller.kt | 19 ++++++++--- .../gitmon/controller/Oauth2Controller.kt | 19 ++++++++--- ...ccessTokenResponse.kt => LoginResponse.kt} | 2 +- .../controller/util/ApiResponseHelper.kt | 32 +++++++++++++++++++ .../ixfp/gitmon/domain/auth/AuthService.kt | 4 +++ 6 files changed, 83 insertions(+), 11 deletions(-) rename src/main/kotlin/com/ixfp/gitmon/controller/response/{AccessTokenResponse.kt => LoginResponse.kt} (80%) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt b/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt index 54745ad..b3f2640 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt @@ -20,7 +20,20 @@ class JwtUtil( ), ) .issuedAt(Date()) - .expiration(Date(System.currentTimeMillis() + TOKEN_EXPIRE_MILLISECONDS)) + .expiration(Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_MILLISECONDS)) + .signWith(key) + .compact() + } + + fun createRefreshToken(memberExposedId: String): String { + return Jwts.builder() + .claims( + mapOf( + "id" to memberExposedId, + ), + ) + .issuedAt(Date()) + .expiration(Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_MILLISECONDS)) .signWith(key) .compact() } @@ -41,7 +54,8 @@ class JwtUtil( } companion object { - private const val TOKEN_EXPIRE_MILLISECONDS = 1000 * 60 * 60 * 10 // 10시간 + private const val ACCESS_TOKEN_EXPIRE_MILLISECONDS = 1000 * 60 * 60 // 1시간 + private const val REFRESH_TOKEN_EXPIRE_MILLISECONDS = 1000 * 60 * 60 * 24 * 14 // 14일 } } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt index 57db0e6..57615a2 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IOauth2Controller.kt @@ -1,7 +1,7 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.controller.request.GithubOauth2Request -import com.ixfp.gitmon.controller.response.AccessTokenResponse +import com.ixfp.gitmon.controller.response.LoginResponse import com.ixfp.gitmon.controller.type.ApiResponseBody import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.headers.Header @@ -34,8 +34,8 @@ interface IOauth2Controller { fun redirectToGithubOauthUrl(profile: String?): ResponseEntity @Operation( - summary = "gitmon Access 토큰 발급", - description = "Github OAuth 인증 코드를 받아 로그인 처리 후 gitmon의 Access Token을 발급 받음.", + summary = "gitmon Access, Refresh 토큰 발급", + description = "Github OAuth 인증 코드를 받아 로그인 처리 후 gitmon의 Access Token을 HTTP Body로 반환 및 Refresh Token 쿠키 설정", ) @ApiResponses( value = [ @@ -59,6 +59,17 @@ interface IOauth2Controller { ), ), ], + headers = [ + Header( + name = "Set-Cookie", + description = "Refresh token cookie", + schema = + Schema( + type = "string", + example = "refreshToken=; HttpOnly; Path=/api/v1/refresh; Secure", + ), + ), + ], ), ApiResponse( responseCode = "401", @@ -83,5 +94,5 @@ interface IOauth2Controller { fun login( profile: String?, request: GithubOauth2Request, - ): ResponseEntity> + ): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 59a64ad..569139c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -3,9 +3,10 @@ package com.ixfp.gitmon.controller import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.common.type.Profile import com.ixfp.gitmon.controller.request.GithubOauth2Request -import com.ixfp.gitmon.controller.response.AccessTokenResponse +import com.ixfp.gitmon.controller.response.LoginResponse import com.ixfp.gitmon.controller.type.ApiResponseBody import com.ixfp.gitmon.controller.util.ApiResponseHelper +import com.ixfp.gitmon.controller.util.withCookie import com.ixfp.gitmon.domain.auth.AuthService import io.github.oshai.kotlinlogging.KotlinLogging.logger import org.springframework.http.ResponseEntity @@ -15,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.time.Duration @RestController @RequestMapping("/api/v1") @@ -32,7 +34,7 @@ class Oauth2Controller( override fun login( @RequestParam profile: String?, @RequestBody request: GithubOauth2Request, - ): ResponseEntity> { + ): ResponseEntity> { val githubAccessToken = githubApiService.getAccessTokenByCode(request.code, Profile.findByCode(profile)) val githubUser = githubApiService.getGithubUser(githubAccessToken) val member = @@ -42,15 +44,24 @@ class Oauth2Controller( authService.login(githubAccessToken, member) val accessToken = authService.createAccessToken(member) + val refreshToken = authService.createRefreshToken(member) val isRepoCreated = member.repoName != null val response = - AccessTokenResponse( + LoginResponse( id = member.exposedId, accessToken = accessToken, isRepoCreated = isRepoCreated, ) - return ApiResponseHelper.success(response) + return ApiResponseHelper.success(response).withCookie( + key = "refreshToken", + value = refreshToken, + maxAge = Duration.ofDays(30), + path = "/api/v1/refresh", + httpOnly = true, + secure = true, + sameSite = "Lax", + ) } companion object { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/LoginResponse.kt similarity index 80% rename from src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt rename to src/main/kotlin/com/ixfp/gitmon/controller/response/LoginResponse.kt index 9151f58..789dbfa 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/response/AccessTokenResponse.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/LoginResponse.kt @@ -1,6 +1,6 @@ package com.ixfp.gitmon.controller.response -data class AccessTokenResponse( +data class LoginResponse( val id: String, val accessToken: String, val isRepoCreated: Boolean, diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt b/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt index d39c464..03c4cda 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/util/ApiResponseHelper.kt @@ -4,7 +4,9 @@ import com.ixfp.gitmon.controller.type.ApiError import com.ixfp.gitmon.controller.type.ApiResponseBody import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus +import org.springframework.http.ResponseCookie import org.springframework.http.ResponseEntity +import java.time.Duration object ApiResponseHelper { fun success(data: T? = null): ResponseEntity> { @@ -44,3 +46,33 @@ object ApiResponseHelper { .body(ApiResponseBody.error(apiError, data)) } } + +fun ResponseEntity>.withCookie( + key: String, + value: String, + maxAge: Duration = Duration.ofDays(14), + path: String = "/api/v1/refresh", + httpOnly: Boolean = true, + secure: Boolean = true, + sameSite: String = "Lax", +): ResponseEntity> { + val cookie = + ResponseCookie.from(key, value) + .path(path) + .httpOnly(httpOnly) + .secure(secure) + .sameSite(sameSite) + .apply { maxAge?.let { maxAge(it) } } + .build() + + val headers = + HttpHeaders().apply { + putAll(this@withCookie.headers) // 기존 헤더 유지 + add(HttpHeaders.SET_COOKIE, cookie.toString()) + } + + return ResponseEntity + .status(this.statusCode) + .headers(headers) + .body(this.body) +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt index ac6615d..c5d2940 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/auth/AuthService.kt @@ -46,6 +46,10 @@ class AuthService( return jwtUtil.createAccessToken(member.exposedId) } + fun createRefreshToken(member: Member): String { + return jwtUtil.createRefreshToken(member.exposedId) + } + fun getGithubAccessToken(memberId: Long): String? { return memberReader.findAccessTokenByMemberId(memberId) } From 9175bf797784f7266f20a377fc3caece21439c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 27 May 2025 16:46:12 +0900 Subject: [PATCH 58/70] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EC=9E=AC?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ixfp/gitmon/controller/AuthController.kt | 58 +++++++++++++++ .../ixfp/gitmon/controller/IAuthController.kt | 72 +++++++++++++++++++ .../controller/response/RefreshResponse.kt | 5 ++ .../ixfp/gitmon/controller/type/ApiError.kt | 1 + 4 files changed, 136 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/controller/response/RefreshResponse.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt new file mode 100644 index 0000000..3c8a89d --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt @@ -0,0 +1,58 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.common.util.JwtUtil +import com.ixfp.gitmon.controller.response.RefreshResponse +import com.ixfp.gitmon.controller.type.ApiError +import com.ixfp.gitmon.controller.type.ApiResponseBody +import com.ixfp.gitmon.controller.util.ApiResponseHelper +import com.ixfp.gitmon.controller.util.withCookie +import com.ixfp.gitmon.domain.auth.AuthService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.CookieValue +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.Duration + +@RestController +@RequestMapping("/api/v1") +class AuthController( + private val authService: AuthService, + private val jwtUtil: JwtUtil, +) : IAuthController { + @PostMapping("/refresh") + override fun refreshToken( + @CookieValue("refreshToken") refreshToken: String?, + ): ResponseEntity> { + println(refreshToken) + if (refreshToken.isNullOrBlank()) { + return ApiResponseHelper.error(ApiError.INVALID_REFRESH_TOKEN) + } + + val exposedId = + jwtUtil.parseAccessToken(refreshToken)?.exposedId + ?: run { + return ApiResponseHelper.error(ApiError.INVALID_REFRESH_TOKEN) + } + + val member = + authService.getMemberByExposedId(exposedId) + ?: run { + return ApiResponseHelper.error(ApiError.INVALID_REFRESH_TOKEN) + } + + val newAccessToken = authService.createAccessToken(member) + val newRefreshToken = authService.createRefreshToken(member) + + return ApiResponseHelper.success(RefreshResponse(newAccessToken)) + .withCookie( + key = "refreshToken", + value = newRefreshToken, + maxAge = Duration.ofDays(30), + path = "/api/v1/refresh", + httpOnly = true, + secure = true, + sameSite = "Lax", + ) + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt new file mode 100644 index 0000000..b6a5b21 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt @@ -0,0 +1,72 @@ +package com.ixfp.gitmon.controller + +import com.ixfp.gitmon.controller.response.RefreshResponse +import com.ixfp.gitmon.controller.type.ApiResponseBody +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.headers.Header +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.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity + +@Tag(name = "Auth Token Control", description = "JWT Access/Refresh 토큰 관련 API") +interface IAuthController { + @Operation( + summary = "토큰 재발급", + description = "Refresh Token 쿠키를 이용해 Access Token, Refresh Token 재발급", + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "토큰 재발급 성공", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "SUCCESS", + "data": { + "accessToken": "header.payload.signature" + } + } + """, + ), + ), + ], + headers = [ + Header( + name = "Set-Cookie", + description = "새로운 Refresh Token 쿠키(선택)", + schema = Schema(type = "string"), + ), + ], + ), + ApiResponse( + responseCode = "401", + description = "만료되었거나 유효하지 않은 Refresh Token", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = + """ + { + "status": "UNAUTHORIZED", + "errorMessage": "Invalid or expired refresh token" + } + """, + ), + ), + ], + ), + ], + ) + fun refreshToken(refreshToken: String?): ResponseEntity> +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/response/RefreshResponse.kt b/src/main/kotlin/com/ixfp/gitmon/controller/response/RefreshResponse.kt new file mode 100644 index 0000000..ae3c68a --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/controller/response/RefreshResponse.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.controller.response + +data class RefreshResponse( + val accessToken: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt index 59d36d2..c7d287c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/type/ApiError.kt @@ -10,6 +10,7 @@ enum class ApiError(val httpStatus: HttpStatus, val message: String) { REPOSITORY_NOT_CONFIGURED(HttpStatus.CONFLICT, "repository not configured"), INVALID_AUTH_CODE(HttpStatus.UNAUTHORIZED, "Invalid auth code"), INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid access token"), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid refresh token"), EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access token expired"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), } From b922f5457edd70d8c355db039732d094bf5f3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 27 May 2025 16:59:28 +0900 Subject: [PATCH 59/70] =?UTF-8?q?chore:=20=ED=86=A0=ED=81=B0=20maxAge=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt | 2 +- src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt index 3c8a89d..b830cbc 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/AuthController.kt @@ -48,7 +48,7 @@ class AuthController( .withCookie( key = "refreshToken", value = newRefreshToken, - maxAge = Duration.ofDays(30), + maxAge = Duration.ofDays(14), path = "/api/v1/refresh", httpOnly = true, secure = true, diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt index 569139c..c9bbc68 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/Oauth2Controller.kt @@ -56,7 +56,7 @@ class Oauth2Controller( return ApiResponseHelper.success(response).withCookie( key = "refreshToken", value = refreshToken, - maxAge = Duration.ofDays(30), + maxAge = Duration.ofDays(14), path = "/api/v1/refresh", httpOnly = true, secure = true, From f632914bbc35495aa0817b0ec324003f906b6236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 27 May 2025 17:01:30 +0900 Subject: [PATCH 60/70] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20=EA=B0=80=EB=8F=85=EC=84=B1=20=ED=96=A5?= =?UTF-8?q?=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ixfp/gitmon/common/util/JwtUtil.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt b/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt index b3f2640..132dd99 100644 --- a/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt +++ b/src/main/kotlin/com/ixfp/gitmon/common/util/JwtUtil.kt @@ -13,19 +13,17 @@ class JwtUtil( private val key = Keys.hmacShaKeyFor(key.toByteArray()) fun createAccessToken(memberExposedId: String): String { - return Jwts.builder() - .claims( - mapOf( - "id" to memberExposedId, - ), - ) - .issuedAt(Date()) - .expiration(Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_MILLISECONDS)) - .signWith(key) - .compact() + return buildToken(memberExposedId, ACCESS_TOKEN_EXPIRE_MILLISECONDS) } fun createRefreshToken(memberExposedId: String): String { + return buildToken(memberExposedId, REFRESH_TOKEN_EXPIRE_MILLISECONDS) + } + + private fun buildToken( + memberExposedId: String, + tokenValidityMillis: Int, + ): String { return Jwts.builder() .claims( mapOf( @@ -33,7 +31,7 @@ class JwtUtil( ), ) .issuedAt(Date()) - .expiration(Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_MILLISECONDS)) + .expiration(Date(System.currentTimeMillis() + tokenValidityMillis)) .signWith(key) .compact() } @@ -54,8 +52,12 @@ class JwtUtil( } companion object { - private const val ACCESS_TOKEN_EXPIRE_MILLISECONDS = 1000 * 60 * 60 // 1시간 - private const val REFRESH_TOKEN_EXPIRE_MILLISECONDS = 1000 * 60 * 60 * 24 * 14 // 14일 + private const val ONE_SECOND_MILLIS = 1000 + private const val ONE_MINUTE_MILLIS = 60 * ONE_SECOND_MILLIS + private const val ONE_HOUR_MILLIS = 60 * ONE_MINUTE_MILLIS + private const val ONE_DAY_MILLIS = 24 * ONE_HOUR_MILLIS + private const val ACCESS_TOKEN_EXPIRE_MILLISECONDS = 1 * ONE_HOUR_MILLIS + private const val REFRESH_TOKEN_EXPIRE_MILLISECONDS = 14 * ONE_DAY_MILLIS } } From cfcce93b24c6e92d85609c2bbc47e220024b56b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Tue, 27 May 2025 17:02:50 +0900 Subject: [PATCH 61/70] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=8B=A8=EC=96=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt index b6a5b21..cc35dd3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IAuthController.kt @@ -42,7 +42,7 @@ interface IAuthController { headers = [ Header( name = "Set-Cookie", - description = "새로운 Refresh Token 쿠키(선택)", + description = "새로운 Refresh Token 쿠키", schema = Schema(type = "string"), ), ], From 19d304fab75fa0918cba7c1721ee84b816d593fe Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Fri, 1 Aug 2025 21:48:31 +0900 Subject: [PATCH 62/70] =?UTF-8?q?[issue#73]=20=EA=B9=83=ED=97=88=EB=B8=8C?= =?UTF-8?q?=20username=20=EC=9C=BC=EB=A1=9C=20=ED=8F=AC=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ixfp/gitmon/controller/IPostingController.kt | 2 ++ .../com/ixfp/gitmon/controller/PostingController.kt | 8 ++++++++ .../com/ixfp/gitmon/domain/member/MemberService.kt | 5 +++++ .../com/ixfp/gitmon/domain/posting/PostingService.kt | 10 ++++++++++ 4 files changed, 25 insertions(+) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 5eb67b7..6168c7e 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -96,4 +96,6 @@ interface IPostingController { ): ResponseEntity> fun getPostingList(exposedMemberId: String): ResponseEntity>> + + fun getPostingListByGithubUsername(githubUsername: String): ResponseEntity>> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 8472038..42cdd5d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -44,6 +44,14 @@ class PostingController( return ApiResponseHelper.success(postingList) } + @GetMapping("/github/{githubUsername}") + override fun getPostingListByGithubUsername( + @PathVariable githubUsername: String, + ): ResponseEntity>> { + val postingList = postingService.findPostingListByGithubUsername(githubUsername) + return ApiResponseHelper.success(postingList) + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt index 7d78ac6..58ce734 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/member/MemberService.kt @@ -20,6 +20,11 @@ class MemberService( ?: throw MemberNotFoundException(message = "exposedId: $exposedId") } + fun getMemberByGithubUsername(githubUsername: String): Member { + return memberReader.findByGithubUsername(githubUsername) + ?: throw MemberNotFoundException(message = "githubUsername: $githubUsername") + } + fun getGithubAccessTokenById(id: Long): String { return memberReader.findAccessTokenByMemberId(id) ?: throw MemberGithubAccessTokenNotFoundException(message = "memberId: $id") diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index d05b24d..a0728b2 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -25,6 +25,16 @@ class PostingService( return postingReader.findPostingListByMemberId(member.id) } + fun findPostingListByGithubUsername(githubUsername: String): List { + log.info { "[PostingService] 포스팅 목록 조회 시작. githubUsername=$githubUsername" } + + val member = memberService.getMemberByGithubUsername(githubUsername) + val postingList = postingReader.findPostingListByMemberId(member.id) + + log.info { "[PostingService] 포스팅 목록 조회 완료. githubUsername=${member.githubUsername}, postingList.size=${postingList.size}" } + return postingList + } + fun create( member: Member, title: String, From 256e28d432b0e042a52f844add9d4ea4d61952b5 Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Fri, 15 Aug 2025 16:58:29 +0900 Subject: [PATCH 63/70] =?UTF-8?q?[issue#77]=20=EA=B9=83=ED=97=88=EB=B8=8C?= =?UTF-8?q?=20username,=20=ED=8F=AC=EC=8A=A4=ED=8C=85=20id=EB=A1=9C=20?= =?UTF-8?q?=ED=8F=AC=EC=8A=A4=ED=8C=85=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ixfp/gitmon/controller/IPostingController.kt | 5 +++++ .../ixfp/gitmon/controller/PostingController.kt | 9 +++++++++ .../ixfp/gitmon/domain/posting/PostingReader.kt | 6 ++++++ .../ixfp/gitmon/domain/posting/PostingService.kt | 15 +++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 6168c7e..6ce7210 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -98,4 +98,9 @@ interface IPostingController { fun getPostingList(exposedMemberId: String): ResponseEntity>> fun getPostingListByGithubUsername(githubUsername: String): ResponseEntity>> + + fun getPosting( + githubUsername: String, + postingId: Long, + ): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 42cdd5d..54ca147 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -52,6 +52,15 @@ class PostingController( return ApiResponseHelper.success(postingList) } + @GetMapping("/github/{githubUsername}/{postingId}") + override fun getPosting( + @PathVariable githubUsername: String, + @PathVariable postingId: Long, + ): ResponseEntity> { + val posting = postingService.findPosting(githubUsername, postingId) + return ApiResponseHelper.success(posting) + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt index cda65a7..1924b8a 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt @@ -15,6 +15,12 @@ class PostingReader( return postingRepository.findByRefMemberId(memberId).map { toPostingItem(it) } } + fun findPostingById(postingId: Long): PostingReadDto { + return postingRepository.findById(postingId) + .orElseThrow { IllegalArgumentException("Posting with id $postingId not found") } + .let { toPostingItem(it) } + } + private fun toPostingItem(entity: PostingEntity): PostingReadDto { return PostingReadDto( id = entity.id, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index a0728b2..08244df 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -35,6 +35,21 @@ class PostingService( return postingList } + fun findPosting( + githubUsername: String, + postingId: Long, + ): PostingReadDto? { + log.info { "[PostingService] 포스팅 조회 시작. githubUsername=$githubUsername, postingId=$postingId" } + + val member = memberService.getMemberByGithubUsername(githubUsername) + val posting = + postingReader.findPostingListByMemberId(member.id) + .find { it.id == postingId } + + log.info { "[PostingService] 포스팅 조회 완료. githubUsername=${member.githubUsername}, posting=$posting" } + return posting + } + fun create( member: Member, title: String, From 386d31eb3b534047ba9e5761f1e86bf59e6638bd Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Fri, 15 Aug 2025 18:24:06 +0900 Subject: [PATCH 64/70] =?UTF-8?q?[issue#81]=20=ED=8F=AC=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20API=20=EC=9D=91=EB=8B=B5=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=83=9D=EC=84=B1=EB=90=9C=20=ED=8F=AC=EC=8A=A4?= =?UTF-8?q?=ED=8C=85=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/controller/IPostingController.kt | 2 +- .../gitmon/controller/PostingController.kt | 7 ++-- .../gitmon/domain/posting/PostingService.kt | 35 ++++++++++++------- .../gitmon/domain/posting/PostingWriter.kt | 6 ++-- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 6ce7210..7a5857d 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -93,7 +93,7 @@ interface IPostingController { title: String, content: MultipartFile, member: Member, - ): ResponseEntity> + ): ResponseEntity> fun getPostingList(exposedMemberId: String): ResponseEntity>> diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 54ca147..268d5d7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -31,9 +31,10 @@ class PostingController( @RequestParam title: String, @RequestPart content: MultipartFile, @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, - ): ResponseEntity> { - postingService.create(member, title, content) - return ApiResponseHelper.created() + ): ResponseEntity> { + val savedPostingId = postingService.create(member, title, content) + val posting = postingService.findPosting(savedPostingId) + return ApiResponseHelper.created(posting) } @GetMapping("/{exposedMemberId}") diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 08244df..f317437 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -35,6 +35,15 @@ class PostingService( return postingList } + fun findPosting(postingId: Long): PostingReadDto? { + log.info { "[PostingService] 포스팅 조회 시작. postingId=$postingId" } + + val posting = postingReader.findPostingById(postingId) + + log.info { "[PostingService] 포스팅 조회 완료. posting=$posting" } + return posting + } + fun findPosting( githubUsername: String, postingId: Long, @@ -54,7 +63,7 @@ class PostingService( member: Member, title: String, content: MultipartFile, - ) { + ): Long { log.info { "PostingService#create start. member=$member, title=$title, content.size=${content.size}" } val githubAccessToken = memberService.getGithubAccessTokenById(member.id) @@ -71,17 +80,19 @@ class PostingService( path = "$title.md", ) - postingWriter.write( - PostingWriteDto( - title = title, - member = member, - githubFilePath = githubContent.path, - githubFileSha = githubContent.sha, - githubDownloadUrl = githubContent.download_url, - ), - ) - - log.info { "PostingService#create end. contentSha=${githubContent.sha}" } + val savedPostingId = + postingWriter.write( + PostingWriteDto( + title = title, + member = member, + githubFilePath = githubContent.path, + githubFileSha = githubContent.sha, + githubDownloadUrl = githubContent.download_url, + ), + ) + + log.info { "PostingService#create end. savedPostingId=$savedPostingId, contentSha=${githubContent.sha}" } + return savedPostingId } private fun uploadImage( diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt index 91424a4..517826c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt @@ -11,7 +11,7 @@ import org.springframework.stereotype.Component class PostingWriter( private val postingRepository: PostingRepository, ) { - fun write(postingWriteDto: PostingWriteDto) { + fun write(postingWriteDto: PostingWriteDto): Long { val entity = PostingEntity( title = postingWriteDto.title, @@ -20,6 +20,8 @@ class PostingWriter( githubFileSha = postingWriteDto.githubFileSha, githubDownloadUrl = postingWriteDto.githubDownloadUrl, ) - postingRepository.save(entity) + val saved = postingRepository.save(entity) + + return saved.id } } From 9848a9e4891b6a3d4cbcd4f5c494b7681b204cfa Mon Sep 17 00:00:00 2001 From: TraceofLight Date: Fri, 15 Aug 2025 18:26:17 +0900 Subject: [PATCH 65/70] feat: ktlint workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github action 사용을 위한 workflow 설정 - 초기 yml 설정 - master hotfix 고려한 target branch --- .github/workflows/ktlint.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/ktlint.yml diff --git a/.github/workflows/ktlint.yml b/.github/workflows/ktlint.yml new file mode 100644 index 0000000..50d24dd --- /dev/null +++ b/.github/workflows/ktlint.yml @@ -0,0 +1,27 @@ +name: Ktlint Check + +on: + pull_request: + branches: + - master + - develop + +jobs: + ktlint: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Ktlint Check + run: ./gradlew ktlintCheck \ No newline at end of file From db8b8c29c963a7a93c3e59666a5d8b7e25f1f82f Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sat, 23 Aug 2025 23:29:22 +0900 Subject: [PATCH 66/70] =?UTF-8?q?CommitMessageGenerator=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/posting/CommitMessageGenerator.kt | 19 +++++++++++++++++++ .../gitmon/domain/posting/PostingService.kt | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/CommitMessageGenerator.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/CommitMessageGenerator.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/CommitMessageGenerator.kt new file mode 100644 index 0000000..85a2182 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/CommitMessageGenerator.kt @@ -0,0 +1,19 @@ +package com.ixfp.gitmon.domain.posting + +object CommitMessageGenerator { + fun createMessage(title: String): String { + return "Create \"$title\" - by Gitmon 👾" + } + + fun updateMessage(title: String): String { + return "Update \"$title\" - by Gitmon 👾" + } + + fun deleteMessage(title: String): String { + return "Delete \"$title\" - by Gitmon 👾" + } + + fun imageUploadMessage(): String { + return "Upload image - by Gitmon 👾" + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index f317437..4ae9fe3 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -78,6 +78,7 @@ class PostingService( githubUsername = member.githubUsername, repo = member.repoName, path = "$title.md", + commitMessage = CommitMessageGenerator.createMessage(title = title), ) val savedPostingId = @@ -116,6 +117,7 @@ class PostingService( githubUsername = member.githubUsername, repo = member.repoName, path = path, + commitMessage = CommitMessageGenerator.imageUploadMessage(), ) return githubContent.download_url From 7c18345118289077f78ffa3b1292db64370793fe Mon Sep 17 00:00:00 2001 From: Suhjeong Kim Date: Sun, 24 Aug 2025 00:13:09 +0900 Subject: [PATCH 67/70] =?UTF-8?q?[issue#84]=20=ED=8F=AC=EC=8A=A4=ED=8C=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitmon/client/github/GithubApiService.kt | 7 ++- .../gitmon/controller/IPostingController.kt | 7 +++ .../gitmon/controller/PostingController.kt | 15 +++++ .../ixfp/gitmon/db/posting/PostingEntity.kt | 8 +-- .../com/ixfp/gitmon/domain/posting/Posting.kt | 14 +++++ .../gitmon/domain/posting/PostingReader.kt | 15 +++-- .../gitmon/domain/posting/PostingService.kt | 62 +++++++++++++++++-- .../gitmon/domain/posting/PostingUpdateDto.kt | 8 +++ .../gitmon/domain/posting/PostingWriter.kt | 19 ++++++ .../posting/exception/PostingExceptions.kt | 5 ++ 10 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingUpdateDto.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 3c493a3..99708ff 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -59,13 +59,14 @@ class GithubApiService( githubUsername: String, repo: String, path: String, - commitMessage: String = "Upsert File by API", + commitMessage: String, + sha: String = "", ): GithubContent { val request = GithubUpsertFileRequest( - message = "Add New File", + message = commitMessage, content = Base64Encoder.encodeBase64(content), - sha = "", + sha = sha, ) val response = githubResourceApiClient.upsertFile( diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 7a5857d..68a3d6f 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -103,4 +103,11 @@ interface IPostingController { githubUsername: String, postingId: Long, ): ResponseEntity> + + fun updatePosting( + id: Long, + title: String, + content: MultipartFile, + member: Member, + ): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 268d5d7..98d76c7 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -12,6 +12,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestAttribute import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam @@ -37,6 +38,20 @@ class PostingController( return ApiResponseHelper.created(posting) } + @PutMapping( + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + ) + override fun updatePosting( + @RequestParam id: Long, + @RequestParam title: String, + @RequestPart content: MultipartFile, + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, + ): ResponseEntity> { + val updatedPosting = postingService.update(member, id, title, content) + val posting = postingService.findPosting(updatedPosting) + return ApiResponseHelper.success(posting) + } + @GetMapping("/{exposedMemberId}") override fun getPostingList( @PathVariable exposedMemberId: String, diff --git a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt index 15c20df..5f7b009 100644 --- a/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt +++ b/src/main/kotlin/com/ixfp/gitmon/db/posting/PostingEntity.kt @@ -13,9 +13,9 @@ class PostingEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, - val title: String, + var title: String, val refMemberId: Long, - val githubFilePath: String, - val githubFileSha: String, - val githubDownloadUrl: String, + var githubFilePath: String, + var githubFileSha: String, + var githubDownloadUrl: String, ) : BaseEntity() diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt new file mode 100644 index 0000000..b0f76b3 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/Posting.kt @@ -0,0 +1,14 @@ +package com.ixfp.gitmon.domain.posting + +import java.time.LocalDateTime + +data class Posting( + val id: Long, + val title: String, + val refMemberId: Long, + val githubFilePath: String, + val githubFileSha: String, + val githubDownloadUrl: String, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt index 1924b8a..beb5066 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingReader.kt @@ -11,20 +11,23 @@ import org.springframework.stereotype.Component class PostingReader( private val postingRepository: PostingRepository, ) { - fun findPostingListByMemberId(memberId: Long): List { - return postingRepository.findByRefMemberId(memberId).map { toPostingItem(it) } + fun findPostingListByMemberId(memberId: Long): List { + return postingRepository.findByRefMemberId(memberId).map { toPosting(it) } } - fun findPostingById(postingId: Long): PostingReadDto { + fun findPostingById(postingId: Long): Posting { return postingRepository.findById(postingId) .orElseThrow { IllegalArgumentException("Posting with id $postingId not found") } - .let { toPostingItem(it) } + .let { toPosting(it) } } - private fun toPostingItem(entity: PostingEntity): PostingReadDto { - return PostingReadDto( + private fun toPosting(entity: PostingEntity): Posting { + return Posting( id = entity.id, title = entity.title, + refMemberId = entity.refMemberId, + githubFilePath = entity.githubFilePath, + githubFileSha = entity.githubFileSha, githubDownloadUrl = entity.githubDownloadUrl, createdAt = entity.createdAt, updatedAt = entity.updatedAt, diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 4ae9fe3..88523da 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -5,6 +5,7 @@ import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.domain.member.Member import com.ixfp.gitmon.domain.member.MemberService import com.ixfp.gitmon.domain.posting.exception.InvalidImageExtensionException +import com.ixfp.gitmon.domain.posting.exception.PostingAuthorizationException import com.ixfp.gitmon.domain.posting.exception.PostingExceptionStrategy import com.ixfp.gitmon.domain.posting.exception.PostingRepositoryNotFoundException import io.github.oshai.kotlinlogging.KotlinLogging.logger @@ -23,6 +24,7 @@ class PostingService( fun findPostingListByMemberExposedId(memberExposedId: String): List { val member = memberService.getMemberByExposedId(memberExposedId) return postingReader.findPostingListByMemberId(member.id) + .map { toPostingReadDto(it) } } fun findPostingListByGithubUsername(githubUsername: String): List { @@ -32,16 +34,16 @@ class PostingService( val postingList = postingReader.findPostingListByMemberId(member.id) log.info { "[PostingService] 포스팅 목록 조회 완료. githubUsername=${member.githubUsername}, postingList.size=${postingList.size}" } - return postingList + return postingList.map { toPostingReadDto(it) } } - fun findPosting(postingId: Long): PostingReadDto? { + fun findPosting(postingId: Long): PostingReadDto { log.info { "[PostingService] 포스팅 조회 시작. postingId=$postingId" } val posting = postingReader.findPostingById(postingId) log.info { "[PostingService] 포스팅 조회 완료. posting=$posting" } - return posting + return toPostingReadDto(posting) } fun findPosting( @@ -56,7 +58,7 @@ class PostingService( .find { it.id == postingId } log.info { "[PostingService] 포스팅 조회 완료. githubUsername=${member.githubUsername}, posting=$posting" } - return posting + return posting?.let { toPostingReadDto(it) } } fun create( @@ -96,6 +98,48 @@ class PostingService( return savedPostingId } + fun update( + member: Member, + postingId: Long, + title: String, + content: MultipartFile, + ): Long { + log.info { "PostingService#update start. member=$member, postingId=$postingId, title=$title, content.size=${content.size}" } + val posting = postingReader.findPostingById(postingId) + + if (posting.refMemberId != member.id) { + log.warn { "본인이 작성하지 않은 포스팅에 대한 수정 시도. memberId=${member.id}, postingId=$postingId" } + throw PostingAuthorizationException() + } + + val githubAccessToken = memberService.getGithubAccessTokenById(member.id) + + val githubContent = + githubApiService.upsertFile( + githubAccessToken = githubAccessToken, + content = content, + githubUsername = member.githubUsername, + repo = member.repoName ?: throw PostingRepositoryNotFoundException(), + path = "$title.md", + commitMessage = CommitMessageGenerator.updateMessage(title = title), + sha = posting.githubFileSha, + ) + + val updatedPostingId = + postingWriter.update( + postingId, + PostingUpdateDto( + title = title, + githubFilePath = githubContent.path, + githubFileSha = githubContent.sha, + githubDownloadUrl = githubContent.download_url, + ), + ) + + log.info { "PostingService#update end. updatedPostingId=$updatedPostingId, contentSha=${githubContent.sha}" } + return updatedPostingId + } + private fun uploadImage( member: Member, content: MultipartFile, @@ -138,6 +182,16 @@ class PostingService( return null } + private fun toPostingReadDto(posting: Posting): PostingReadDto { + return PostingReadDto( + id = posting.id, + title = posting.title, + githubDownloadUrl = posting.githubDownloadUrl, + createdAt = posting.createdAt, + updatedAt = posting.updatedAt, + ) + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingUpdateDto.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingUpdateDto.kt new file mode 100644 index 0000000..173a90a --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingUpdateDto.kt @@ -0,0 +1,8 @@ +package com.ixfp.gitmon.domain.posting + +data class PostingUpdateDto( + val title: String, + val githubFilePath: String, + val githubFileSha: String, + val githubDownloadUrl: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt index 517826c..16477d4 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingWriter.kt @@ -4,6 +4,7 @@ import com.ixfp.gitmon.common.aop.WrapWith import com.ixfp.gitmon.db.exception.DbExceptionStrategy import com.ixfp.gitmon.db.posting.PostingEntity import com.ixfp.gitmon.db.posting.PostingRepository +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component @WrapWith(DbExceptionStrategy::class) @@ -24,4 +25,22 @@ class PostingWriter( return saved.id } + + fun update( + postingId: Long, + postingUpdateDto: PostingUpdateDto, + ): Long { + val postingEntity = + postingRepository.findByIdOrNull(postingId) + ?: throw IllegalArgumentException("Posting with id $postingId not found") + + postingEntity.title = postingUpdateDto.title + postingEntity.githubFilePath = postingUpdateDto.githubFilePath + postingEntity.githubFileSha = postingUpdateDto.githubFileSha + postingEntity.githubDownloadUrl = postingUpdateDto.githubDownloadUrl + + val updated = postingRepository.save(postingEntity) + + return updated.id + } } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt index 3c60937..ba6a6a1 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/exception/PostingExceptions.kt @@ -17,6 +17,11 @@ class InvalidImageExtensionException( cause: Throwable? = null, ) : PostingException(message, cause) +class PostingAuthorizationException( + message: String = "게시글에 대한 권한이 없습니다.", + cause: Throwable? = null, +) : PostingException(message, cause) + class PostingUnexpectedException( message: String = "예상치 못한 게시글 도메인 예외가 발생했습니다.", cause: Throwable? = null, From 8433d34f7d7e8c2457f05885767ae3ff296a09bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Sun, 7 Sep 2025 16:59:25 +0900 Subject: [PATCH 68/70] =?UTF-8?q?feat:=20=ED=8F=AC=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ixfp/gitmon/config/web/WebMvcConfig.kt | 1 + .../gitmon/controller/IPostingController.kt | 77 +++++++++++++++++++ .../gitmon/controller/PostingController.kt | 12 +++ .../gitmon/domain/posting/PostingService.kt | 2 +- 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt index b0e2787..47c8f92 100644 --- a/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt +++ b/src/main/kotlin/com/ixfp/gitmon/config/web/WebMvcConfig.kt @@ -14,6 +14,7 @@ class WebMvcConfig( .addPathPatterns("/api/v1/member/repo/**") // 인증이 필요한 경로 .addPathPatterns("/api/v1/member", "/api/v1/member/") .addPathPatterns("/api/v1/posting", "/api/v1/posting/") + .addPathPatterns("/api/v1/posting/images", "/api/v1/posting/images/") } override fun addCorsMappings(registry: CorsRegistry) { diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt index 68a3d6f..4fe1c44 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/IPostingController.kt @@ -110,4 +110,81 @@ interface IPostingController { content: MultipartFile, member: Member, ): ResponseEntity> + + @Operation( + summary = "이미지 업로드", + description = "포스팅에 사용할 이미지를 업로드합니다.", + security = [SecurityRequirement(name = ACCESS_TOKEN)], + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "201", + description = "이미지 업로드 성공", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "CREATED", + "data": "https://raw.githubusercontent.com/user/repo/main/images/uuid.jpg" + } + """, + ), + ), + ], + ), + ApiResponse( + responseCode = "400", + description = "허용되지 않은 이미지 확장자", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "BAD_REQUEST", + "errorMessage": "invalid image extension" + } + """, + ), + ), + ], + ), + ApiResponse( + responseCode = "409", + description = "사용자의 레포지토리가 아직 설정되지 않음", + content = [ + Content( + mediaType = "application/json", + schema = + Schema( + example = """ + { + "status": "CONFLICT", + "errorMessage": "repository not configured" + } + """, + ), + ), + ], + ), + ], + ) + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "업로드할 이미지 파일", + required = true, + content = [ + Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + ), + ], + ) + fun uploadImage( + image: MultipartFile, + member: Member, + ): ResponseEntity> } diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt index 98d76c7..08a1923 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/PostingController.kt @@ -77,6 +77,18 @@ class PostingController( return ApiResponseHelper.success(posting) } + @PostMapping( + "/images", + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + ) + override fun uploadImage( + @RequestPart image: MultipartFile, + @RequestAttribute(AUTHENTICATED_MEMBER) member: Member, + ): ResponseEntity> { + val imageUrl = postingService.uploadImage(member, image) + return ApiResponseHelper.created(imageUrl) + } + companion object { private val log = logger {} } diff --git a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt index 88523da..7db461c 100644 --- a/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/domain/posting/PostingService.kt @@ -140,7 +140,7 @@ class PostingService( return updatedPostingId } - private fun uploadImage( + fun uploadImage( member: Member, content: MultipartFile, ): String { From c6f8f84c22e195e5f82523d5abf24e0fbd8ae857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Sun, 7 Sep 2025 16:59:37 +0900 Subject: [PATCH 69/70] =?UTF-8?q?chore:=20dev=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/resources/application-dev.yml diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..746eaf3 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +oauth2: + client: + github: + id: ${GITHUB_OAUTH2_CLIENT_ID_DEV} + secret: ${GITHUB_OAUTH2_CLIENT_SECRET_DEV} From a30d13994f3e878a5c6bf641bf646d47f26f2d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=98=81=EC=9A=B0?= Date: Sun, 7 Sep 2025 18:39:40 +0900 Subject: [PATCH 70/70] =?UTF-8?q?chore:=20=EB=A9=80=ED=8B=B0=ED=8C=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=2010MB=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본: 1MB --- src/main/resources/application.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 51edf67..f02373a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,10 @@ spring: application: name: gitmon + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB datasource: url: ${DATABASE_URL} username: ${DATABASE_USERNAME}