Skip to content

Issue:(#9, #10) cors 설정 변경 및 온보딩 구현 #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 15, 2025

Conversation

HoyeongJeon
Copy link
Collaborator

@HoyeongJeon HoyeongJeon commented Apr 14, 2025

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📝 작업 내용

  • 온보딩 구현

✏️ 관련 이슈


🎸 기타 사항 or 추가 코멘트

처음 회원가입 후 유저 커플 연결 안되어있으면 ROLE이 GUEST야 그래서 온보딩까지 끝내야 MEMBER로 변해
그래서 온보딩 API는 Role이 GUEST만 사용 가능해
코드보고 이해 안되는 부분 있으면 물어봐

Summary by CodeRabbit

  • New Features
    • 신규 온보딩 기능 도입: 사용자가 닉네임과 생일 정보를 업데이트하여 계정 등급 전환을 진행할 수 있습니다.
    • 회원 정보 조회 기능 개선: 개인 정보가 보다 명확하게 제공됩니다.
    • 로그인 프로세스 개선: 이메일 기반 인증과 닉네임 활용이 적용되었으며, OAuth2 로그인 후 프로덕션 환경으로 리다이렉션됩니다.
    • 추가 프론트엔드 도메인 지원으로 보안 설정이 강화되었습니다.
    • 새로운 API 경로 상수 정의 및 회원 정보 관련 REST 컨트롤러 추가.
  • Refactor
    • 인증 토큰에 사용자 역할 정보가 포함되어, 보안 및 권한 관리가 개선되었습니다.
    • 사용자 세부 정보 로딩 프로세스가 이메일 기반으로 변경되어 명확성이 향상되었습니다.

@HoyeongJeon HoyeongJeon self-assigned this Apr 14, 2025
Copy link

coderabbitai bot commented Apr 14, 2025

Walkthrough

이번 변경 사항은 여러 구성 요소의 코드를 수정 및 개선한 PR입니다. Gradle 종속성 버전이 다운그레이드되고, 사용자 권한 및 식별 관련 메서드들이 업데이트되었으며, API 응답의 스키마 주석이 추가되었습니다. 또한, CORS 및 보안 설정에 새 엔드포인트와 인증 규칙이 도입되었고, JWT 토큰 생성 과정에 사용자 역할이 포함되도록 변경되었습니다. Swagger 관련 설정과 Unauthorized 예외 처리는 제거되었으며, 회원 정보 조회 및 온보딩 기능을 위한 새로운 Facade, 서비스, 컨트롤러, DTO, 테스트 클래스가 추가되었습니다.

Changes

Files 변경 요약
build.gradle.kts springdoc-openapi-starter-webmvc-ui 버전이 2.8.1에서 2.7.0으로 다운그레이드됨
src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt
src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt
src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt
사용자 권한 반환 방식 및 사용자 식별자(nickname) 관련 메서드 수정, loadUserByUsername 매개변수를 email로 변경
src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt
src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt
src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt
ApiResponse에 @Schema(hidden = true) 주석 추가, Swagger 구성 파일 및 UnauthorizedException 제거
src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt
src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt
src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt
CORS allowedOrigins 확장, /v1/member/onboarding 엔드포인트 추가 및 인증 규칙 강화, JWT 필터에서 Swagger 관련 URI 추가
src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt
src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt
provideAccessToken 및 createToken 메서드 시그니처에 role 파라미터 추가 및 토큰에 role 클레임 포함
src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt
src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt
토큰 생성 시 role 전달, 리다이렉트 URL을 프로덕션 주소로 변경, OAuth2 사용자 처리에서 이름 대신 nickname 및 역할 상수 업데이트
src/main/kotlin/gomushin/backend/member/… Member 엔티티가 name에서 nickname으로 변경, birthDate 및 role 프로퍼티 추가, findByName 제거 및 회원 정보·온보딩 기능을 위한 Facade, 서비스, 컨트롤러, DTO, API 경로 상수, Role enum 추가
src/test/kotlin/gomushin/backend/member/… MemberInfoFacade, OnboardingFacade, MemberInfoService, OnboardingService 테스트 클래스 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트
    participant OC as OnboardingController
    participant OF as OnboardingFacade
    participant OS as OnboardingService
    participant MR as MemberRepository

    Client->>OC: POST /v1/member/onboarding
    OC->>OF: onboarding(customUserDetails, onboardingRequest)
    OF->>OS: onboarding(id, onboardingRequest)
    OS->>MR: findById(id)
    MR-->>OS: Member 데이터 반환
    OS-->>OF: 업데이트된 Member 정보
    OF-->>OC: 처리 결과 반환
    OC-->>Client: ApiResponse(Boolean)
Loading
sequenceDiagram
    participant Client as 클라이언트
    participant MIC as MemberInfoController
    participant MIF as MemberInfoFacade
    participant MIS as MemberInfoService
    participant MR as MemberRepository

    Client->>MIC: GET /v1/member/my-info
    MIC->>MIF: getMemberInfo(customUserDetails)
    MIF->>MIS: getGuestInfo(id)
    MIS->>MR: findById(id)
    MR-->>MIS: Member 데이터 반환
    MIS-->>MIF: Member 정보 반환
    MIF-->>MIC: GuestInfoResponse 생성
    MIC-->>Client: ApiResponse(Member 정보)
Loading

Poem

작은 토끼가 뛰어놀아요,
코드 숲에 새싹 같은 변화가 피어나요.
의존성은 낮아지고, 권한과 이름은 반짝이며 바뀌었네,
API와 JWT도 리듬에 맞춰 춤을 추어요.
온보딩의 문이 활짝 열리고, 회원 정보도 빛나네요.
이 귀여운 변화에 모두 함께 기뻐해요! 🐇✨

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.

📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4084fe and fe00c1a.

📒 Files selected for processing (4)
  • src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt (2 hunks)
  • src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt (4 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt
  • src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt
  • src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt
  • src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🔭 Outside diff range comments (1)
src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt (1)

62-70: 🛠️ Refactor suggestion

UserDTO와 Member 클래스 간의 속성명 불일치

UserDTO는 여전히 name 속성을 사용하는 반면, Member 클래스는 이제 nickname을 사용합니다. 이로 인해 혼란이 생길 수 있습니다.

 val userDto = UserDTO(
     username = oAuth2Response.getProviderId(),
-    name = oAuth2Response.getName(),
+    nickname = oAuth2Response.getName(),
     email = oAuth2Response.getEmail(),
     profileImage = oAuth2Response.getProfileImage(),
     role = Role.MEMBER.name,
     registrationId = registrationId,
     userId = savedMember.id,
 )

UserDTO 클래스에서도 namenickname으로 변경하는 것이 일관성을 유지하는 데 도움이 될 것입니다.

🧹 Nitpick comments (9)
src/main/kotlin/gomushin/backend/member/domain/value/Role.kt (1)

3-6: KDoc 문서화 추가를 고려해보세요.

각 역할(GUEST, MEMBER)의 목적과 권한을 설명하는 KDoc 문서를 추가하면 다른 개발자들이 코드를 더 쉽게 이해할 수 있을 것입니다.

 enum class Role {
+    /**
+     * 온보딩을 완료하지 않은 초기 사용자 역할
+     */
     GUEST,
+    /**
+     * 온보딩을 완료하고 모든 기능을 사용할 수 있는 역할
+     */
     MEMBER,
 }
src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt (1)

31-33: Role enum 타입 활용 검토가 필요합니다.

현재 getRole() 메서드는 String 타입을 반환하고 있는데, 새로 추가된 Role enum을 활용하여 타입 안전성을 높이는 것을 고려해보세요. 이렇게 하면 컴파일 시점에서 타입 오류를 발견할 수 있습니다.

- fun getRole() : String {
-     return userDto.role
- }
+ fun getRole() : Role {
+     return Role.valueOf(userDto.role)
+ }

또는 UserDTO 클래스에서 role 필드의 타입을 String에서 Role로 변경하는 것도 고려해볼 수 있습니다.

src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt (1)

7-13: 온보딩 기능을 위한 파사드 패턴 적용

온보딩 기능을 위한 파사드 클래스가 적절히 구현되었습니다. 파사드 패턴을 통해 온보딩 관련 복잡한 로직을 캡슐화하고 컨트롤러에 단순한 인터페이스를 제공합니다.

한 가지 제안: 파사드 메서드가 단순히 서비스 메서드를 호출하는 것만으로는 파사드의 이점이 제한될 수 있습니다. 필요한 경우 트랜잭션 관리나 추가 비즈니스 로직을 포함하는 것을 고려해 보세요.

src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt (1)

1-18: 회원 정보 조회 서비스 구현

회원 정보를 조회하는 서비스 클래스가 적절하게 구현되었습니다. Spring 표준 아키텍처 패턴을 따르고 있으며, 데이터 조회는 읽기 전용 트랜잭션으로 표시되어 있습니다.

한 가지 주의할 점은 예외 메시지 "sarangggun.member.not-exist-member"에 오타가 있는 것 같습니다('g'가 한 글자 더 들어감). 메시지 키를 확인하고 올바르게 수정하는 것이 좋겠습니다.

-    fun getGuestInfo(id: Long): Member =
-        memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") }
+    fun getGuestInfo(id: Long): Member =
+        memberRepository.findById(id).orElseThrow { BadRequestException("saranggun.member.not-exist-member") }
src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt (1)

1-26: 회원 정보 조회 컨트롤러 구현

회원 정보를 조회하는 컨트롤러가 적절하게 구현되었습니다. REST 패턴을 준수하고 있으며, Swagger 문서화를 위한 어노테이션도 잘 적용되어 있습니다. 현재 인증된 사용자의 정보를 가져오기 위해 @AuthenticationPrincipal을 사용하는 것도 좋은 방법입니다.

반환 타입이 ApiResponse<Any>로 설정되어 있는데, 타입 안전성을 높이기 위해 실제 반환되는 객체 타입으로 지정하는 것이 좋을 것 같습니다.

-    fun get(
-        @AuthenticationPrincipal customUserDetails: CustomUserDetails
-    ): ApiResponse<Any> {
+    fun get(
+        @AuthenticationPrincipal customUserDetails: CustomUserDetails
+    ): ApiResponse<GuestInfoResponse> {
src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt (2)

52-62: GUEST 사용자에 대한 정보 조회가 적절히 테스트되고 있습니다.

회원 정보 조회 테스트가 잘 구현되어 있습니다. 하지만 현재의 테스트는 GuestInfoResponse로의 변환과 닉네임 검증만 수행하고 있습니다.

개선 가능한 부분:

  1. 61번 라인에서 result를 GuestInfoResponse로 캐스팅할 때, 타입 검사를 먼저 수행하는 것이 더 안전할 것 같습니다.
  2. 닉네임뿐만 아니라 응답의 다른 속성들도 검증하면 좋을 것 같습니다.
 // then
 verify(memberInfoService).getGuestInfo(1L)
-assertEquals(member.nickname, (result as GuestInfoResponse).nickname)
+val response = result as? GuestInfoResponse
+requireNotNull(response) { "Response should be of type GuestInfoResponse" }
+assertEquals(member.nickname, response.nickname)
+// 다른 필드들에 대한 검증도 추가

32-50: setUp 메서드가 효율적으로 구성되어 있습니다.

테스트 전 초기화를 위한 @beforeeach 메서드가 잘 구현되어 있습니다. 하지만 mock 객체 설정 방식에 일관성이 부족합니다.

개선 사항:

  • customUserDetails를 mock()으로 생성하는 대신, @mock 어노테이션을 사용하면 코드가 더 일관성 있어 보일 것 같습니다.
 @ExtendWith(MockitoExtension::class)
 class MemberInfoFacadeTest {
     @Mock
     private lateinit var memberInfoService: MemberInfoService
+    @Mock
+    private lateinit var customUserDetails: CustomUserDetails

     @InjectMocks
     private lateinit var memberInfoFacade: MemberInfoFacade

-    private lateinit var customUserDetails: CustomUserDetails
     private lateinit var member: Member

     @BeforeEach
     fun setUp() {
         member = Member(
             id = 1L,
             nickname = "테스트 닉네임",
             email = "[email protected]",
             birthDate = null,
             profileImageUrl = null,
             provider = Provider.KAKAO,
             role = Role.GUEST,
         )

         val authorities = mutableListOf<GrantedAuthority>(SimpleGrantedAuthority("ROLE_GUEST"))
-        customUserDetails = mock(CustomUserDetails::class.java)

         `when`(customUserDetails.getId()).thenReturn(1L)
         `when`(customUserDetails.authorities).thenReturn(authorities)
     }
src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt (1)

16-17: 예외 처리 개선 필요

현재 구현에서는 회원이 존재하지 않을 경우 BadRequestException을 발생시키고 있습니다. 하지만 이는 클라이언트 오류보다는 서버 측 리소스를 찾을 수 없는 상황이므로 NotFoundException이 더 적절할 수 있습니다.

-val member = memberRepository.findById(id).orElseThrow { BadRequestException("sarangggun.member.not-exist-member") }
+val member = memberRepository.findById(id).orElseThrow { NotFoundException("sarangggun.member.not-exist-member") }

참고: NotFoundException 클래스가 프로젝트에 정의되어 있는지 확인하고, 없다면 추가해야 합니다.

src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt (1)

25-26: JWT 토큰에 역할 정보가 추가되었습니다. 유효성 검사를 고려하세요.

사용자 역할을 JWT 토큰에 포함시키는 것은 좋은 변경 사항입니다. 이를 통해 권한 검사가 더 효율적으로 이루어질 수 있습니다.

다음과 같이 역할 매개변수에 대한 유효성 검사를 추가하는 것이 좋습니다:

override fun provideAccessToken(userId: Long, role: String): String {
+   require(role.isNotBlank()) { "역할은 비어 있을 수 없습니다." }
    return createToken(userId, role, ACCESS_TOKEN_EXPIRATION, Type.ACCESS)
}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00b9519 and d4084fe.

📒 Files selected for processing (30)
  • build.gradle.kts (1 hunks)
  • src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt (2 hunks)
  • src/main/kotlin/gomushin/backend/core/configuration/security/CustomCorsConfiguration.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt (3 hunks)
  • src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt (0 hunks)
  • src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt (0 hunks)
  • src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt (2 hunks)
  • src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt (3 hunks)
  • src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt (4 hunks)
  • src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/application/MemberInfoFacade.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/application/OnboardingFacade.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt (2 hunks)
  • src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt (0 hunks)
  • src/main/kotlin/gomushin/backend/member/domain/service/MemberInfoService.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/domain/service/OnboardingService.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/domain/value/Role.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/MemberInfoController.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/OnboardingController.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt (1 hunks)
  • src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt (1 hunks)
  • src/test/kotlin/gomushin/backend/member/application/MemberInfoFacadeTest.kt (1 hunks)
  • src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt (1 hunks)
  • src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt (1 hunks)
  • src/test/kotlin/gomushin/backend/member/domain/service/OnboardingServiceTest.kt (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/kotlin/gomushin/backend/member/domain/repository/MemberRepository.kt
  • src/main/kotlin/gomushin/backend/core/infrastructure/exception/UnauthorizedException.kt
  • src/main/kotlin/gomushin/backend/core/configuration/swagger/SwaggerConfiguration.kt
🔇 Additional comments (30)
src/main/kotlin/gomushin/backend/member/domain/value/Role.kt (1)

3-6: Enum 클래스 구현이 적절합니다.

이 Role enum 클래스는 GUEST와 MEMBER 두 가지 역할을 명확하게 정의하고 있어 적절합니다. PR 설명에 언급된 것처럼 사용자가 처음 등록할 때는 GUEST 역할이 되고, 온보딩 과정을 완료하면 MEMBER 역할로 전환된다는 비즈니스 로직을 잘 표현하고 있습니다.

src/main/kotlin/gomushin/backend/core/oauth/CustomOAuth2User.kt (1)

31-33: OAuth2User에 역할 정보 제공 기능이 추가되었습니다.

새로 추가된 getRole() 메서드를 통해 사용자의 역할 정보를 문자열로 반환하도록 구현되었습니다. 이는 JWT 토큰 생성 시 사용자 역할 정보를 포함하고, Spring Security의 인증/인가 처리에 활용될 것으로 보입니다.

build.gradle.kts (1)

37-37:

❓ Verification inconclusive

SpringDoc OpenAPI 의존성 버전이 다운그레이드되었습니다.

springdoc-openapi-starter-webmvc-ui 의존성이 2.8.1에서 2.7.0으로 다운그레이드되었습니다. 이는 의도적인 결정인지 확인이 필요합니다.

혹시 이 버전 다운그레이드가 필요한 이유가 있는지요? 일반적으로 의존성 버전은 가능한 최신 버전을 사용하는 것이 보안 및 기능적인 측면에서 좋습니다. 다운그레이드의 이유가 특정 호환성 문제 해결이라면 주석으로 이유를 남겨두는 것이 좋습니다.


🏁 Script executed:

#!/bin/bash
# SpringDoc OpenAPI 버전 관련 정보 확인

echo "현재 선택된 버전: 2.7.0"
echo "이전 버전: 2.8.1"

# 두 버전의 Maven Central 정보 확인
echo -e "\n2.7.0 버전 정보:"
curl -s "https://search.maven.org/solrsearch/select?q=g:org.springdoc+AND+a:springdoc-openapi-starter-webmvc-ui+AND+v:2.7.0&wt=json" | jq '.response.docs[0]'

echo -e "\n2.8.1 버전 정보:"
curl -s "https://search.maven.org/solrsearch/select?q=g:org.springdoc+AND+a:springdoc-openapi-starter-webmvc-ui+AND+v:2.8.1&wt=json" | jq '.response.docs[0]'

# 해당 패키지의 최신 버전 확인
echo -e "\n최신 버전 정보:"
curl -s "https://search.maven.org/solrsearch/select?q=g:org.springdoc+AND+a:springdoc-openapi-starter-webmvc-ui&rows=1&wt=json" | jq '.response.docs[0]'

Length of output: 1836


확인 요청: SpringDoc OpenAPI 의존성 버전 다운그레이드에 대한 의도 검증

  • 현재 build.gradle.kts 파일 37번째 줄에서 springdoc-openapi-starter-webmvc-ui 의존성이 2.8.1에서 2.7.0으로 다운그레이드되어 적용되었습니다.
  • Maven Central의 정보에 따르면, 2.7.0과 2.8.1 버전 모두 유효하며, 최신 버전은 2.8.6 입니다.
  • 해당 다운그레이드가 특정 호환성 이슈를 해결하기 위한 의도적인 결정인지, 아니면 실수로 인한 변경인지 확인 부탁드립니다. 만약 의도한 변경이라면 관련 이유(예: 호환성 문제 등)를 주석으로 남겨두시길 권장합니다.
src/main/kotlin/gomushin/backend/member/presentation/dto/request/OnboardingRequest.kt (1)

6-12: 온보딩 요청 DTO가 적절하게 정의되었습니다.

닉네임과 생일 정보를 받는 OnboardingRequest DTO가 잘 정의되어 있습니다. Swagger 문서화를 위한 @Schema 어노테이션도 적절히 사용되었습니다.

src/main/kotlin/gomushin/backend/core/jwt/JwtTokenProvider.kt (1)

4-4: 인터페이스 변경이 잘 되었습니다.

토큰 생성 시 사용자 역할(role)을 포함하도록 provideAccessToken 메서드 시그니처가 적절히 수정되었습니다. 이는 PR 목표인 온보딩 과정에서의 역할 관리를 위한 필수적인 변경사항입니다.

src/main/kotlin/gomushin/backend/core/infrastructure/filter/JwtAuthenticationFilter.kt (1)

45-45: 포맷팅이 개선되었습니다.

코드 가독성을 위한 빈 줄 추가는 좋은 변경입니다.

src/main/kotlin/gomushin/backend/member/presentation/ApiPath.kt (1)

3-6: API 경로 상수화 구현이 좋습니다.

API 경로를 상수로 정의한 것은 유지보수성과 일관성을 높이는 좋은 방법입니다. 향후 더 많은 API 경로가 추가될 때도 이 패턴을 유지하면 좋겠습니다.

온보딩 엔드포인트(ONBOARDING)와 회원 정보 조회 엔드포인트(MY_INFO)가 명확하게 정의되어 있습니다. 이는 PR 목표인 온보딩 구현을 위한 좋은 기반입니다.

src/main/kotlin/gomushin/backend/core/oauth/handler/CustomSuccessHandler.kt (3)

37-37: 토큰 생성 시 역할 정보 추가

사용자의 역할 정보를 토큰에 포함시키는 변경사항입니다. 이는 PR의 목적인 온보딩 프로세스와 역할 관리(GUEST → MEMBER)를 지원하기 위한 중요한 변경입니다.


39-39: 미등록 사용자의 토큰에도 역할 정보 추가

사용자가 시스템에 등록되지 않은 경우에도 OAuth2User에서 가져온 역할 정보를 토큰에 포함시키는 것으로 변경되었습니다. 이는 일관된 인증 흐름을 제공합니다.


43-43: 리다이렉트 URL 변경

개발 환경(localhost)에서 프로덕션 환경(Vercel)으로 리다이렉트 URL이 변경되었습니다. 프로덕션 환경에 배포하기 위한 준비 단계로 보입니다.

src/main/kotlin/gomushin/backend/member/presentation/dto/response/GuestInfoResponse.kt (1)

5-11: 게스트 정보 응답 DTO 구현

게스트 사용자의 정보를 반환하기 위한 응답 DTO가 적절히 구현되었습니다. 팩토리 메서드 패턴을 사용하여 Member 엔티티에서 간편하게 변환할 수 있는 of 메서드를 제공하는 것이 좋습니다.

다만, 현재는 닉네임만 포함하고 있는데, 향후 게스트에 대한 추가 정보가 필요한 경우 확장이 용이합니다.

src/main/kotlin/gomushin/backend/core/common/web/response/ApiResponse.kt (1)

8-8: API 문서에서 오류 필드 숨김 처리

Swagger/OpenAPI 문서에서 오류 필드를 숨기도록 @Schema(hidden = true) 주석을 추가했습니다. 이는 API 문서를 더 깔끔하게 유지하고 클라이언트에게 필요한 정보만 노출하는 좋은 관행입니다.

이 변경은 API 응답 형식을 변경하지 않고 문서화에만 영향을 미칩니다.

Also applies to: 24-24

src/main/kotlin/gomushin/backend/core/configuration/security/SecurityConfiguration.kt (3)

5-5: 이전에 주석 처리되었던 CustomSuccessHandler 가져오기 추가

주석 처리되었던 CustomSuccessHandler 가져오기가 활성화되었습니다. 이는 OAuth2 로그인 성공 시 JWT 토큰 발급과 관련된 핸들러로 보이며, 53번 라인에서 사용되고 있습니다.


21-21: 매개변수 뒤에 쉼표 추가

매개변수 목록의 마지막 항목 뒤에 쉼표를 추가했습니다. 이는 Kotlin 스타일 가이드를 따르는 것으로, 향후 추가 매개변수를 추가할 때 코드 변경을 최소화하는 좋은 습관입니다.


70-71: 권한 관리 규칙 추가

온보딩 엔드포인트(/v1/member/onboarding)에 대한 접근 권한을 "GUEST" 역할로 제한하고, 기타 모든 요청에 인증을 요구하는 규칙을 추가했습니다. 이는 PR의 목표(GUEST 역할을 가진 사용자만 온보딩 API에 접근할 수 있도록 함)와 일치합니다.

src/test/kotlin/gomushin/backend/member/application/OnboardingFacadeTest.kt (1)

1-35: 테스트 코드가 기본적인 요구사항을 충족합니다.

이 테스트는 OnboardingFacade가 OnboardingService에 요청을 올바르게 위임하는지 확인하고 있습니다. 기본적인 테스트 구조와, given-when-then 패턴을 잘 따르고 있습니다.

다만, 다음과 같은 개선 사항을 고려해 볼 수 있습니다:

  1. 예외 상황에 대한 테스트 케이스가 없습니다. 오류 시나리오도 테스트하면 좋을 것 같습니다.
  2. 만약 Facade에서 데이터 변환 로직이 있다면, 반환값에 대한 검증도 추가하면 좋을 것 같습니다.
src/test/kotlin/gomushin/backend/member/domain/service/MemberInfoServiceTest.kt (1)

1-48: 테스트 구조가 잘 갖추어져 있습니다.

MemberInfoService의 getGuestInfo 메서드에 대한 기본적인 테스트가 잘 작성되어 있습니다. Mockito를 활용한 의존성 주입과 테스트 구조가 적절합니다.

개선 가능한 부분:

  1. 현재는 성공 케이스만 테스트하고 있는데, 회원을 찾을 수 없는 경우와 같은 예외 상황에 대한 테스트도 추가하면 좋을 것 같습니다.
  2. 회원의 역할(Role)이 GUEST가 아닌 경우에 대한 동작 검증도 필요할 수 있습니다.
src/main/kotlin/gomushin/backend/core/oauth/service/CustomOAuth2UserService.kt (3)

10-10: Role 열거형 도입을 통한 역할 관리 개선

Role 열거형을 사용하여 역할 관리를 개선한 것이 좋습니다. 이는 하드코딩된 문자열 대신 타입 안전성을 제공합니다.


36-36: Member 클래스의 속성명 변경 반영

기존 name 속성에서 nickname으로의 변경이 일관되게 적용되었습니다.


46-46: 역할 지정 개선

하드코딩된 "ROLE_USER" 대신 Role.MEMBER.name을 사용하여 일관성과 유지보수성이 향상되었습니다.

src/main/kotlin/gomushin/backend/member/domain/entity/Member.kt (5)

16-17: 속성명 변경이 적절히 수행됨

name에서 nickname으로의 속성명 변경이 잘 수행되었습니다. 이는 사용자 식별에 더 적합한 용어입니다.


19-20: email 필드에 non-null 제약 조건 추가

이메일 필드에 nullable = false 제약 조건을 추가한 것은 데이터 무결성을 위해 좋은 변경입니다.


22-24: birthDate 필드 추가

사용자의 생년월일을 저장하기 위한 필드가 추가되었습니다. 이는 온보딩 과정에서 수집되는 정보로 적절합니다.


32-34: 역할 관리를 위한 필드 추가

Role 열거형을 사용하여 사용자 역할을 관리하는 것은 좋은 접근 방식입니다. 또한 기본값으로 Role.GUEST를 설정한 것은 PR 설명에 부합합니다.


16-50: 커플 정보 필드 부재

PR 설명에 따르면 "유저 커플에 연결되지 않은 경우 GUEST 역할을 가진다"고 되어 있습니다. 그러나 현재 Member 엔티티에는 커플 정보를 저장하는 필드가 보이지 않습니다.

커플 정보와 관련된 필드나 관계가 다른 파일에 정의되어 있는지 확인해 보세요. 만약 없다면, 다음과 같은 관계를 추가하는 것이 좋을 수 있습니다:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "couple_id")
var couple: Couple? = null

또는:

@Column(name = "couple_id")
var coupleId: Long? = null

이를 통해 사용자가 커플에 연결되어 있는지 여부를 추적할 수 있습니다.

src/main/kotlin/gomushin/backend/core/CustomUserDetails.kt (3)

11-13: 역할 기반 권한 부여 개선

역할에 기반한 권한 부여 메커니즘이 잘 구현되었습니다. SimpleGrantedAuthority를 사용하여 멤버의 역할을 기반으로 권한을 생성하는 것은 Spring Security의 모범 사례입니다.


19-21: 사용자 식별자 필드 업데이트

Member 엔티티에서 namenickname으로 변경됨에 따라 getUsername() 메서드도 적절히 업데이트되었습니다.


12-12:

✅ Verification successful

프리픽스 중복 가능성 확인 필요

"ROLE_${member.role.name}"의 구현에서, Role 열거형의 이름에 이미 "ROLE_" 프리픽스가 포함되어 있는지 확인이 필요합니다. 그렇지 않으면 괜찮지만, 만약 이미 포함되어 있다면 "ROLE_ROLE_MEMBER"와 같은 중복이 발생할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# Role 열거형에 이미 "ROLE_" 프리픽스가 포함되어 있는지 확인
rg -l "enum\s+class\s+Role" | xargs cat | grep -A 10 "enum class Role"

Length of output: 115


검증 결과: 접두어 중복 문제 없음

역할(Role) 열거형의 값이 "GUEST"와 "MEMBER"처럼 접두어 없이 정의되어 있으므로,
CustomUserDetails.kt의 다음 코드

return mutableListOf(SimpleGrantedAuthority("ROLE_${member.role.name}"))

는 올바른 구현입니다. "ROLE_" 접두어가 enum 값에 포함되어 있지 않으므로 중복 문제가 발생하지 않습니다.

src/main/kotlin/gomushin/backend/core/jwt/infrastructure/JwtTokenProviderImpl.kt (1)

59-59: 역할에 대한 클레임이 추가되었습니다.

JWT 토큰에 역할 클레임을 추가한 것은 권한 관리에 있어 좋은 접근 방식입니다.

src/main/kotlin/gomushin/backend/core/service/CustomUserDetailsService.kt (1)

18-21: 사용자 ID 기반 조회 메서드가 더 간결해졌습니다.

createUserDetail 메서드 제거로 코드가 더 간결해졌고, CustomUserDetails를 직접 생성하는 방식으로 변경된 것은 좋은 리팩토링입니다.

다만, 여기서도 같은 예외 메시지를 사용하고 있어 이전 코멘트에서 언급한 것과 같은 보안 고려사항이 적용됩니다.

Comment on lines +1 to +58
package gomushin.backend.member.domain.service

import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.repository.MemberRepository
import gomushin.backend.member.domain.value.Provider
import gomushin.backend.member.domain.value.Role
import gomushin.backend.member.presentation.dto.request.OnboardingRequest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate
import java.util.*
import kotlin.test.Test

@ExtendWith(MockitoExtension::class)
class OnboardingServiceTest {
@Mock
private lateinit var memberRepository: MemberRepository

private val onboardingService: OnboardingService by lazy {
OnboardingService(memberRepository)
}

@Test
fun `onboarding 성공 케이스`() {
// given
val memberId = 1L
val existingMember = Member(
id = memberId,
nickname = "원래 닉네임",
email = "[email protected]",
birthDate = LocalDate.of(1990, 1, 1),
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
)

val onboardingRequest = OnboardingRequest(
nickname = "새로운 닉네임",
birthDate = LocalDate.of(2000, 1, 1)
)

`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(existingMember))

// when
onboardingService.onboarding(memberId, onboardingRequest)

// then
assertEquals("새로운 닉네임", existingMember.nickname)
assertEquals(LocalDate.of(2000, 1, 1), existingMember.birthDate)
assertEquals(Role.MEMBER, existingMember.role)

verify(memberRepository).findById(memberId)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

온보딩 기능의 핵심 동작이 테스트되고 있습니다.

온보딩 서비스의 동작을 검증하는 테스트가 적절히 작성되어 있으며, 회원의 닉네임, 생년월일 업데이트 및 역할 변경(GUEST → MEMBER)을 확인하고 있습니다.

다음과 같은 개선 사항을 고려해 보세요:

  1. findById 호출 여부는 검증하고 있지만, 변경된 회원 정보를 저장하는 memberRepository.save() 메서드 호출 여부는 검증되지 않았습니다. 이 부분도 verify로 확인하면 좋을 것 같습니다.
  2. 회원을 찾을 수 없는 경우나, 이미 GUEST가 아닌 역할을 가진 회원에 대한 예외 케이스도 테스트하면 더 완성도 높은 테스트가 될 것 같습니다.
 // then
 assertEquals("새로운 닉네임", existingMember.nickname)
 assertEquals(LocalDate.of(2000, 1, 1), existingMember.birthDate)
 assertEquals(Role.MEMBER, existingMember.role)

 verify(memberRepository).findById(memberId)
+verify(memberRepository).save(existingMember)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package gomushin.backend.member.domain.service
import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.repository.MemberRepository
import gomushin.backend.member.domain.value.Provider
import gomushin.backend.member.domain.value.Role
import gomushin.backend.member.presentation.dto.request.OnboardingRequest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate
import java.util.*
import kotlin.test.Test
@ExtendWith(MockitoExtension::class)
class OnboardingServiceTest {
@Mock
private lateinit var memberRepository: MemberRepository
private val onboardingService: OnboardingService by lazy {
OnboardingService(memberRepository)
}
@Test
fun `onboarding 성공 케이스`() {
// given
val memberId = 1L
val existingMember = Member(
id = memberId,
nickname = "원래 닉네임",
email = "[email protected]",
birthDate = LocalDate.of(1990, 1, 1),
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
)
val onboardingRequest = OnboardingRequest(
nickname = "새로운 닉네임",
birthDate = LocalDate.of(2000, 1, 1)
)
`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(existingMember))
// when
onboardingService.onboarding(memberId, onboardingRequest)
// then
assertEquals("새로운 닉네임", existingMember.nickname)
assertEquals(LocalDate.of(2000, 1, 1), existingMember.birthDate)
assertEquals(Role.MEMBER, existingMember.role)
verify(memberRepository).findById(memberId)
}
}
package gomushin.backend.member.domain.service
import gomushin.backend.member.domain.entity.Member
import gomushin.backend.member.domain.repository.MemberRepository
import gomushin.backend.member.domain.value.Provider
import gomushin.backend.member.domain.value.Role
import gomushin.backend.member.presentation.dto.request.OnboardingRequest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import java.time.LocalDate
import java.util.*
import kotlin.test.Test
@ExtendWith(MockitoExtension::class)
class OnboardingServiceTest {
@Mock
private lateinit var memberRepository: MemberRepository
private val onboardingService: OnboardingService by lazy {
OnboardingService(memberRepository)
}
@Test
fun `onboarding 성공 케이스`() {
// given
val memberId = 1L
val existingMember = Member(
id = memberId,
nickname = "원래 닉네임",
email = "[email protected]",
birthDate = LocalDate.of(1990, 1, 1),
profileImageUrl = null,
provider = Provider.KAKAO,
role = Role.GUEST,
)
val onboardingRequest = OnboardingRequest(
nickname = "새로운 닉네임",
birthDate = LocalDate.of(2000, 1, 1)
)
`when`(memberRepository.findById(memberId)).thenReturn(Optional.of(existingMember))
// when
onboardingService.onboarding(memberId, onboardingRequest)
// then
assertEquals("새로운 닉네임", existingMember.nickname)
assertEquals(LocalDate.of(2000, 1, 1), existingMember.birthDate)
assertEquals(Role.MEMBER, existingMember.role)
verify(memberRepository).findById(memberId)
verify(memberRepository).save(existingMember)
}
}

Copy link
Contributor

@kimyeoungrok kimyeoungrok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고했어요~~~!

@@ -34,7 +34,7 @@ dependencies {
runtimeOnly("mysql:mysql-connector-java:8.0.33")

// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.1")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

버전을 내린 이유가 궁금해!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

버전을 내린 이유가 궁금해!

아 이건 Swagger 응답이 좀 이상하게 나와서 버전 문제인 줄 알고 내렸거든.
근데 버전 문제는 아니었어
이건 내가 내렸다 안 올렸네
스웨거 2.8.1 이나 2.7.0 은 사용하는데 별 차이는 없으니까 그냥 가도 될 것 같아

@@ -12,7 +12,8 @@ class CustomCorsConfiguration {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://localhost:5173", "http://localhost:8080")
configuration.allowedOrigins =
listOf("http://localhost:5173", "http://localhost:8080", "https://frontend-sarang.vercel.app")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트에서 로컬로 개발할 때 5173포트쓰나? 보통 3000번 포트 쓰지 않아?

Copy link
Collaborator Author

@HoyeongJeon HoyeongJeon Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트에서 로컬로 개발할 때 5173포트쓰나? 보통 3000번 포트 쓰지 않아?

프론트 포트 5173이래

}

response!!.addCookie(createCookie("access_token", accessToken))
response.sendRedirect("http://localhost:8080") // TODO: 프론트엔드 주소로 변경 , 환경변수 처리
response.sendRedirect("https://frontend-sarang.vercel.app")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 프론트는 로컬환경에서 개발할건데 이렇게 하면 프론트입자에서 로컬에 테스트 할때 토큰을 못 받지 않나? 바로 배포주소로 리디렉션되니깐?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 프론트는 로컬환경에서 개발할건데 이렇게 하면 프론트입자에서 로컬에 테스트 할때 토큰을 못 받지 않나? 바로 배포주소로 리디렉션되니깐?

아 맞네 이것도 프론트 주소로 바꿀게
근데 우리 배포가 이미 둘 다 https 면 프론트에서 로컬로 쿠키를 못 받는데…
이건 고민 해봐야겠다

private val memberRepository: MemberRepository
) {

@Transactional
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여긴 왜 readonly=true를 안 쓰는 거야?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여긴 왜 readonly=true를 안 쓰는 거야?

JPA 변경감지를 통해서 Member를 업데이트 하는 코드인데, 여기에 readonly = true 를 달면 엔티티 스냅샷을 안만들거든.
그러면 변경감지가 안되니까 업데이트도 안돼.

간단하게 말하면 조회 메서드가 아닌 업데이트 메서드라서 안썼다~

@HoyeongJeon HoyeongJeon merged commit 9ed0c7e into main Apr 15, 2025
1 check passed
@HoyeongJeon HoyeongJeon deleted the issue/#9-cors-config branch April 15, 2025 05:03
@coderabbitai coderabbitai bot mentioned this pull request Apr 16, 2025
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cors 설정
2 participants