Skip to content

Commit

Permalink
Merge branch 'main' into feat/mypage
Browse files Browse the repository at this point in the history
  • Loading branch information
wonseok committed Jan 31, 2025
2 parents 3e6d91f + 111e760 commit d75bf24
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 19 deletions.
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 우리 프로젝트 이름

이 프로젝트는 온라인 중고거래 및 커뮤니티 플랫폼인 "당근 마켓"을 클론한 프로젝트입니다. 기존의 중고거래 기능에 더해 **경매 기능**을 추가하여 사용자들이 물품을 경매 형식으로 거래할 수 있도록 확장했습니다. 이 프로젝트는 필수 스펙과 권장 스펙을 충족하며, 새로운 기능인 경매를 통해 사용자 경험을 향상시켰습니다. [서비스 바로가기](https://toykarrot.shop/)

---

## 팀원 소개

- **김정훈**: 소셜 로그인, 환경 설정, 보안 설정
- **박원석**:
- **이준용**:

---

## 클론 코딩 필수 스펙

### <span>**필수 스펙**</span>

- **회원가입 / 로그인 / 소셜 로그인**
- 닉네임, 아이디, 비밀번호, 이메일을 통한 회원가입
- 소셜 로그인 (Google, Kakao, Naver) 지원
- **유저 계정 페이지**
- 프로필 수정 (사진, 닉네임, 동네)
- 매너온도 확인
- 판매내역 조회
- 내 매너 평가 및 거래 후기 조회
- **글 작성 / 댓글 작성**
- 중고거래 및 동네생활 게시글 작성
- 댓글 및 댓글 좋아요 기능
- **페이지네이션**
- 게시글 목록 및 댓글 목록에 페이지네이션 적용
- **AWS 배포**
- EC2와 S3를 통한 프로젝트 배포

### <span>**권장 스펙**</span>

- **HTTPS 설정**
- 보안 강화를 위한 HTTPS 적용
- **Github Actions CI/CD**
- 자동화된 배포 및 테스트를 위한 CI/CD 파이프라인 구축

---

## 새로운 기능: **경매**

- **경매 물품 올리기**
- 판매자가 경매 물품을 등록할 수 있습니다.
- 시작가와 경매 종료 시간을 설정할 수 있습니다.
- **경매 참여**
- 구매자는 경매에 참여하여 입찰할 수 있습니다.
- 입찰 가격은 시작가의 5% 배수로 인상 가능합니다.
- **경매 종료**
- 제한 시간 내에 가장 높은 가격을 부른 구매자에게 물품이 판매됩니다.

---

## 전체 기능

### 회원가입
- 닉네임, 아이디, 비밀번호, 이메일을 받아 회원가입을 진행합니다.

### 로그인
- 일반 로그인 및 소셜 로그인을 지원합니다.

### 동네 설정
- 사용자의 동네를 설정합니다. 아직 지역 기반 서비스는 구현하지 못했습니다.

### 홈 (중고거래)
- **물품 올리기**: 판매자가 물품을 등록할 수 있습니다.
- **물품 조회**: 현재 판매중인 모든 물품을 최신순으로 보여줍니다.
- **물품 관심 기능**: 관심 있는 물품을 저장할 수 있습니다.
- **판매자와 채팅**: 구매자와 판매자가 실시간으로 채팅할 수 있습니다.

### 동네 생활
- **글쓰기**: 사용자가 동네 생활 게시글을 작성할 수 있습니다.
- **글 조회**: 게시글을 조회하고 댓글을 작성할 수 있습니다.
- **댓글 좋아요**: 댓글에 좋아요를 누를 수 있습니다.
- **공감**: 게시글에 공감을 표시할 수 있습니다.

### 경매 (새로운 기능)
- **경매 물품 올리기**: 판매자가 경매 물품을 등록합니다.
- **경매 참여**: 구매자가 경매에 참여하여 입찰합니다.
- **경매 종료**: 가장 높은 가격을 부른 구매자에게 물품이 판매됩니다.

### 채팅
- **채팅 기록 저장**: 물건을 구매할 때 채팅했던 기록이 저장됩니다.
- **실시간 채팅**: WebSocket을 이용하여 실시간 채팅을 구현하였습니다.

### 나의 당근
- **프로필 수정**: 사진, 닉네임, 동네를 수정할 수 있습니다.
- **매너온도 확인**: 사용자의 매너온도를 확인할 수 있습니다.
- **판매내역**: 판매한 물품의 내역을 조회할 수 있습니다.
- **내 매너 평가 조회**: 다른 사용자로부터 받은 매너 평가를 확인할 수 있습니다.
- **거래 후기 조회**: 거래 후기를 조회할 수 있습니다.

### 타사용자 프로필
- **매너 칭찬**: 다른 사용자의 매너를 칭찬할 수 있습니다.
- **판매물품 목록**: 해당 사용자가 판매 중인 물품을 조회할 수 있습니다.
- **받은 매너 평가**: 해당 사용자가 받은 매너 평가를 확인할 수 있습니다.
- **받은 거래 후기 조회**: 해당 사용자가 받은 거래 후기를 조회할 수 있습니다.
1 change: 0 additions & 1 deletion src/main/kotlin/com/toyProject7/karrot/SecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class SecurityConfig(
registry
.requestMatchers(
*SecurityConstants.PUBLIC_PATHS,
"/ws/**",
).permitAll()
.anyRequest().authenticated()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -15,7 +16,10 @@ class MannerController(
@PathVariable nickname: String,
@PathVariable mannerType: MannerType,
): ResponseEntity<String> {
mannerService.increaseMannerCount(nickname, mannerType)
// Decode the nickname
val decodedNickname = URLDecoder.decode(nickname, "UTF-8")

mannerService.increaseMannerCount(decodedNickname, mannerType)
return ResponseEntity.noContent().build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -37,7 +38,10 @@ class ProfileController(
fun getProfile(
@PathVariable nickname: String,
): ResponseEntity<ProfileResponse> {
val profile = profileService.getProfile(nickname)
// Decode nickname
val decodedNickname = URLDecoder.decode(nickname, "UTF-8")

val profile = profileService.getProfile(decodedNickname)
return ResponseEntity.ok(profile)
}

Expand All @@ -46,7 +50,10 @@ class ProfileController(
@PathVariable nickname: String,
@RequestParam articleId: Long,
): ResponseEntity<List<Item>> {
val itemList: List<Item> = profileService.getProfileSells(nickname, articleId)
// Decode nickname
val decodedNickname = URLDecoder.decode(nickname, "UTF-8")

val itemList: List<Item> = profileService.getProfileSells(decodedNickname, articleId)
return ResponseEntity.ok(itemList)
}

Expand All @@ -63,7 +70,10 @@ class ProfileController(
fun getManners(
@PathVariable nickname: String,
): ResponseEntity<MannersResponse> {
val manners = profileService.getManner(nickname)
// Decode nickname
val decodedNickname = URLDecoder.decode(nickname, "UTF-8")

val manners = profileService.getManner(decodedNickname)
return ResponseEntity.ok(manners)
}

Expand All @@ -72,7 +82,10 @@ class ProfileController(
@PathVariable nickname: String,
@RequestParam("reviewId") reviewId: Long,
): ResponseEntity<ReviewsResponse> {
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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ object SecurityConstants {
"/auth/**",
"/login/oauth2/**",
"/api/test",
"/ws/**",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -86,19 +85,19 @@ class CustomAuthenticationSuccessHandler(
}
}

private fun extractName(
/*private fun extractName(
attributes: Map<String, Any>,
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<*, *>
profile["nickname"] as String
}
else -> "Unknown"
}
}
}*/
}
31 changes: 26 additions & 5 deletions src/main/kotlin/com/toyProject7/karrot/user/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down
4 changes: 1 addition & 3 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand All @@ -41,7 +40,6 @@ spring:
redirectUri: "https://toykarrot.shop/{action}/oauth2/code/{registrationId}"
scope:
- profile_nickname
- profile_image
- account_email
clientName: Kakao
provider:
Expand Down

0 comments on commit d75bf24

Please sign in to comment.