diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d8a7a6 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# 우리 프로젝트 이름 + +이 프로젝트는 온라인 중고거래 및 커뮤니티 플랫폼인 "당근 마켓"을 클론한 프로젝트입니다. 기존의 중고거래 기능에 더해 **경매 기능**을 추가하여 사용자들이 물품을 경매 형식으로 거래할 수 있도록 확장했습니다. 이 프로젝트는 필수 스펙과 권장 스펙을 충족하며, 새로운 기능인 경매를 통해 사용자 경험을 향상시켰습니다. [서비스 바로가기](https://toykarrot.shop/) + +--- + +## 팀원 소개 + +- **김정훈**: 소셜 로그인, 환경 설정, 보안 설정 +- **박원석**: +- **이준용**: + +--- + +## 클론 코딩 필수 스펙 + +### **필수 스펙** + +- **회원가입 / 로그인 / 소셜 로그인** + - 닉네임, 아이디, 비밀번호, 이메일을 통한 회원가입 + - 소셜 로그인 (Google, Kakao, Naver) 지원 +- **유저 계정 페이지** + - 프로필 수정 (사진, 닉네임, 동네) + - 매너온도 확인 + - 판매내역 조회 + - 내 매너 평가 및 거래 후기 조회 +- **글 작성 / 댓글 작성** + - 중고거래 및 동네생활 게시글 작성 + - 댓글 및 댓글 좋아요 기능 +- **페이지네이션** + - 게시글 목록 및 댓글 목록에 페이지네이션 적용 +- **AWS 배포** + - EC2와 S3를 통한 프로젝트 배포 + +### **권장 스펙** + +- **HTTPS 설정** + - 보안 강화를 위한 HTTPS 적용 +- **Github Actions CI/CD** + - 자동화된 배포 및 테스트를 위한 CI/CD 파이프라인 구축 + +--- + +## 새로운 기능: **경매** + +- **경매 물품 올리기** + - 판매자가 경매 물품을 등록할 수 있습니다. + - 시작가와 경매 종료 시간을 설정할 수 있습니다. +- **경매 참여** + - 구매자는 경매에 참여하여 입찰할 수 있습니다. + - 입찰 가격은 시작가의 5% 배수로 인상 가능합니다. +- **경매 종료** + - 제한 시간 내에 가장 높은 가격을 부른 구매자에게 물품이 판매됩니다. + +--- + +## 전체 기능 + +### 회원가입 +- 닉네임, 아이디, 비밀번호, 이메일을 받아 회원가입을 진행합니다. + +### 로그인 +- 일반 로그인 및 소셜 로그인을 지원합니다. + +### 동네 설정 +- 사용자의 동네를 설정합니다. 아직 지역 기반 서비스는 구현하지 못했습니다. + +### 홈 (중고거래) +- **물품 올리기**: 판매자가 물품을 등록할 수 있습니다. +- **물품 조회**: 현재 판매중인 모든 물품을 최신순으로 보여줍니다. +- **물품 관심 기능**: 관심 있는 물품을 저장할 수 있습니다. +- **판매자와 채팅**: 구매자와 판매자가 실시간으로 채팅할 수 있습니다. + +### 동네 생활 +- **글쓰기**: 사용자가 동네 생활 게시글을 작성할 수 있습니다. +- **글 조회**: 게시글을 조회하고 댓글을 작성할 수 있습니다. +- **댓글 좋아요**: 댓글에 좋아요를 누를 수 있습니다. +- **공감**: 게시글에 공감을 표시할 수 있습니다. + +### 경매 (새로운 기능) +- **경매 물품 올리기**: 판매자가 경매 물품을 등록합니다. +- **경매 참여**: 구매자가 경매에 참여하여 입찰합니다. +- **경매 종료**: 가장 높은 가격을 부른 구매자에게 물품이 판매됩니다. + +### 채팅 +- **채팅 기록 저장**: 물건을 구매할 때 채팅했던 기록이 저장됩니다. +- **실시간 채팅**: WebSocket을 이용하여 실시간 채팅을 구현하였습니다. + +### 나의 당근 +- **프로필 수정**: 사진, 닉네임, 동네를 수정할 수 있습니다. +- **매너온도 확인**: 사용자의 매너온도를 확인할 수 있습니다. +- **판매내역**: 판매한 물품의 내역을 조회할 수 있습니다. +- **내 매너 평가 조회**: 다른 사용자로부터 받은 매너 평가를 확인할 수 있습니다. +- **거래 후기 조회**: 거래 후기를 조회할 수 있습니다. + +### 타사용자 프로필 +- **매너 칭찬**: 다른 사용자의 매너를 칭찬할 수 있습니다. +- **판매물품 목록**: 해당 사용자가 판매 중인 물품을 조회할 수 있습니다. +- **받은 매너 평가**: 해당 사용자가 받은 매너 평가를 확인할 수 있습니다. +- **받은 거래 후기 조회**: 해당 사용자가 받은 거래 후기를 조회할 수 있습니다. diff --git a/src/main/kotlin/com/toyProject7/karrot/SecurityConfig.kt b/src/main/kotlin/com/toyProject7/karrot/SecurityConfig.kt index 48aed6e..e676376 100644 --- a/src/main/kotlin/com/toyProject7/karrot/SecurityConfig.kt +++ b/src/main/kotlin/com/toyProject7/karrot/SecurityConfig.kt @@ -35,7 +35,6 @@ class SecurityConfig( registry .requestMatchers( *SecurityConstants.PUBLIC_PATHS, - "/ws/**", ).permitAll() .anyRequest().authenticated() } diff --git a/src/main/kotlin/com/toyProject7/karrot/manner/controller/MannerController.kt b/src/main/kotlin/com/toyProject7/karrot/manner/controller/MannerController.kt index 3af6fd2..225c1e9 100644 --- a/src/main/kotlin/com/toyProject7/karrot/manner/controller/MannerController.kt +++ b/src/main/kotlin/com/toyProject7/karrot/manner/controller/MannerController.kt @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RestController +import java.net.URLDecoder @RestController class MannerController( @@ -15,7 +16,10 @@ class MannerController( @PathVariable nickname: String, @PathVariable mannerType: MannerType, ): ResponseEntity { - mannerService.increaseMannerCount(nickname, mannerType) + // Decode the nickname + val decodedNickname = URLDecoder.decode(nickname, "UTF-8") + + mannerService.increaseMannerCount(decodedNickname, mannerType) return ResponseEntity.noContent().build() } } diff --git a/src/main/kotlin/com/toyProject7/karrot/profile/controller/ProfileController.kt b/src/main/kotlin/com/toyProject7/karrot/profile/controller/ProfileController.kt index 4403a10..87f90c6 100644 --- a/src/main/kotlin/com/toyProject7/karrot/profile/controller/ProfileController.kt +++ b/src/main/kotlin/com/toyProject7/karrot/profile/controller/ProfileController.kt @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.net.URLDecoder @RestController class ProfileController( @@ -37,7 +38,10 @@ class ProfileController( fun getProfile( @PathVariable nickname: String, ): ResponseEntity { - val profile = profileService.getProfile(nickname) + // Decode nickname + val decodedNickname = URLDecoder.decode(nickname, "UTF-8") + + val profile = profileService.getProfile(decodedNickname) return ResponseEntity.ok(profile) } @@ -46,7 +50,10 @@ class ProfileController( @PathVariable nickname: String, @RequestParam articleId: Long, ): ResponseEntity> { - val itemList: List = profileService.getProfileSells(nickname, articleId) + // Decode nickname + val decodedNickname = URLDecoder.decode(nickname, "UTF-8") + + val itemList: List = profileService.getProfileSells(decodedNickname, articleId) return ResponseEntity.ok(itemList) } @@ -63,7 +70,10 @@ class ProfileController( fun getManners( @PathVariable nickname: String, ): ResponseEntity { - val manners = profileService.getManner(nickname) + // Decode nickname + val decodedNickname = URLDecoder.decode(nickname, "UTF-8") + + val manners = profileService.getManner(decodedNickname) return ResponseEntity.ok(manners) } @@ -72,7 +82,10 @@ class ProfileController( @PathVariable nickname: String, @RequestParam("reviewId") reviewId: Long, ): ResponseEntity { - val reviews = profileService.getPreviousReviews(nickname, reviewId) + // Decode nickname + val decodedNickname = URLDecoder.decode(nickname, "UTF-8") + + val reviews = profileService.getPreviousReviews(decodedNickname, reviewId) return ResponseEntity.ok(reviews) } } diff --git a/src/main/kotlin/com/toyProject7/karrot/security/SecurityConstants.kt b/src/main/kotlin/com/toyProject7/karrot/security/SecurityConstants.kt index 0da6517..2977c13 100644 --- a/src/main/kotlin/com/toyProject7/karrot/security/SecurityConstants.kt +++ b/src/main/kotlin/com/toyProject7/karrot/security/SecurityConstants.kt @@ -8,5 +8,6 @@ object SecurityConstants { "/auth/**", "/login/oauth2/**", "/api/test", + "/ws/**", ) } diff --git a/src/main/kotlin/com/toyProject7/karrot/socialLogin/handler/CustomAuthenticationSuccessHandler.kt b/src/main/kotlin/com/toyProject7/karrot/socialLogin/handler/CustomAuthenticationSuccessHandler.kt index b59095b..319c3f5 100644 --- a/src/main/kotlin/com/toyProject7/karrot/socialLogin/handler/CustomAuthenticationSuccessHandler.kt +++ b/src/main/kotlin/com/toyProject7/karrot/socialLogin/handler/CustomAuthenticationSuccessHandler.kt @@ -31,10 +31,9 @@ class CustomAuthenticationSuccessHandler( val attributes = oauth2User.attributes val providerId = extractProviderId(attributes, provider) val email = extractEmail(attributes, provider) - val name = extractName(attributes, provider) // Create or retrieve the user - val user = userService.createOrRetrieveSocialUser(email, providerId, provider, name) + val user = userService.createOrRetrieveSocialUser(email, providerId, provider) // Generate JWT val accessToken = UserAccessTokenUtil.generateAccessToken(user.id) @@ -86,13 +85,13 @@ class CustomAuthenticationSuccessHandler( } } - private fun extractName( + /*private fun extractName( attributes: Map, provider: String, ): String { return when (provider) { "google" -> attributes["name"] as String - "naver" -> (attributes["response"] as Map<*, *>)["name"] as String + "naver" -> (attributes["response"] as Map<*, *>)["nickname"] as String "kakao" -> { val kakaoAccount = attributes["kakao_account"] as Map<*, *> val profile = kakaoAccount["profile"] as Map<*, *> @@ -100,5 +99,5 @@ class CustomAuthenticationSuccessHandler( } else -> "Unknown" } - } + }*/ } diff --git a/src/main/kotlin/com/toyProject7/karrot/user/service/UserService.kt b/src/main/kotlin/com/toyProject7/karrot/user/service/UserService.kt index bd6269f..e3ca5a6 100644 --- a/src/main/kotlin/com/toyProject7/karrot/user/service/UserService.kt +++ b/src/main/kotlin/com/toyProject7/karrot/user/service/UserService.kt @@ -108,16 +108,26 @@ class UserService( email: String, providerId: String, provider: String, - username: String, ): User { // Check if the user exists by email val existingUser = userRepository.findSocialUserByEmail(email) return existingUser?.let { - // Convert existingUser (of type SocialUser) to User DTO + // If the user exists, convert to User DTO and return User.fromEntity(it) } ?: run { - // If the user doesn't exist, create a new one + // If the user doesn't exist, generate a unique random username + var username = generateRandomString() + var isUnique = false + + while (!isUnique) { + if (!userRepository.existsByNickname(username)) { + isUnique = true + } + username = generateRandomString() + } + + // Create a new SocialUser with the generated username val newUser = SocialUser( email = email, @@ -129,18 +139,29 @@ class UserService( imageUrl = null, updatedAt = Instant.now(), ) - val savedUser = userRepository.save(newUser) // This should save as SocialUser + // Save the new user + val savedUser = userRepository.save(newUser) + + // Create and save the associated profile val profileEntity = ProfileEntity( user = newUser, ) profileService.saveProfileEntity(profileEntity) - User.fromEntity(savedUser) // Convert and return as User DTO + // Convert and return as User DTO + User.fromEntity(savedUser) } } + private fun generateRandomString(length: Int = 8): String { + val characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return (1..length) + .map { characters.random() } + .joinToString("") + } + @Transactional fun getUserEntityById(id: String): UserEntity { return userRepository.findByIdOrNull(id) ?: throw UserNotFoundException() diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d6fa4fe..5fed258 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -29,9 +29,8 @@ spring: authorizationGrantType: authorization_code redirectUri: "https://toykarrot.shop/{action}/oauth2/code/{registrationId}" scope: - - nickname + - profile - email - - profile_image clientName: Naver kakao: clientId: '${KAKAO_CLI_ID}' @@ -41,7 +40,6 @@ spring: redirectUri: "https://toykarrot.shop/{action}/oauth2/code/{registrationId}" scope: - profile_nickname - - profile_image - account_email clientName: Kakao provider: