Skip to content

Commit

Permalink
Merge branch 'main' into feat/chat
Browse files Browse the repository at this point in the history
  • Loading branch information
wonseok committed Jan 31, 2025
2 parents f127b1f + 1540c0e commit 0a3593e
Show file tree
Hide file tree
Showing 21 changed files with 247 additions and 110 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
sudo docker rm $(sudo docker ps -aq)
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/karrot
sudo docker run -d -p 8080:8080 \
--memory="768m" --cpus="0.8" \
-e JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \
-e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
-e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
Expand All @@ -107,4 +108,4 @@ jobs:
-e KAKAO_CLI_ID=${{ secrets.KAKAO_CLI_ID }} \
-e KAKAO_CLI_SECRET=${{ secrets.KAKAO_CLI_SECRET }} \
${{ secrets.DOCKER_USERNAME }}/karrot
sudo docker image prune -f
sudo docker system prune -a -f --volumes
15 changes: 11 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
FROM openjdk:17
# FROM openjdk:17
# ARG JAR_FILE=build/libs/karrot-0.0.1-SNAPSHOT.jar
# COPY ${JAR_FILE} /app.jar
# EXPOSE 8080
# ENTRYPOINT ["java", "-jar", "/app.jar"]

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
ARG JAR_FILE=build/libs/karrot-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Xmx512m", "-Xms256m", "-jar", "app.jar"]
EXPOSE 8080
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 @@ -23,8 +23,20 @@ class AuctionPermissionDeniedException : AuctionException(
msg = "Permission denied",
)

class AuctionBadPriceException : AuctionException(
class AuctionTooLowPriceException : AuctionException(
errorCode = 0,
httpStatusCode = HttpStatus.BAD_REQUEST,
msg = "Your price is lower than current price",
)

class AuctionTooFineUnitExceptions : AuctionException(
errorCode = 0,
httpStatusCode = HttpStatus.BAD_REQUEST,
msg = "Minimum unit is 5% of the start price",
)

class AuctionOverException : AuctionException(
errorCode = 0,
httpStatusCode = HttpStatus.BAD_REQUEST,
msg = "Over auction time",
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package com.toyProject7.karrot.auction.service

import com.toyProject7.karrot.article.ArticlePermissionDeniedException
import com.toyProject7.karrot.article.controller.UpdateStatusRequest
import com.toyProject7.karrot.auction.AuctionBadPriceException
import com.toyProject7.karrot.auction.AuctionNotFoundException
import com.toyProject7.karrot.auction.AuctionOverException
import com.toyProject7.karrot.auction.AuctionPermissionDeniedException
import com.toyProject7.karrot.auction.AuctionTooFineUnitExceptions
import com.toyProject7.karrot.auction.AuctionTooLowPriceException
import com.toyProject7.karrot.auction.controller.Auction
import com.toyProject7.karrot.auction.controller.AuctionMessage
import com.toyProject7.karrot.auction.controller.PostAuctionRequest
Expand Down Expand Up @@ -32,15 +34,21 @@ class AuctionService(
@Transactional
fun updatePrice(auctionMessage: AuctionMessage): AuctionMessage {
val auctionEntity = auctionRepository.findByIdOrNull(auctionMessage.auctionId) ?: throw AuctionNotFoundException()
if (Instant.now().isAfter(auctionEntity.endTime)) {
throw AuctionOverException()
}
if (auctionEntity.currentPrice >= auctionMessage.price) {
throw AuctionBadPriceException()
throw AuctionTooLowPriceException()
}
if ((auctionMessage.price - auctionEntity.currentPrice) % (auctionEntity.startingPrice * 0.05).toInt() != 0) {
throw AuctionTooFineUnitExceptions()
}
val bidder = userService.getUserEntityByNickname(auctionMessage.senderNickname)

auctionEntity.currentPrice = auctionMessage.price
auctionEntity.bidder = bidder
if (ChronoUnit.MINUTES.between(auctionEntity.endTime, Instant.now()) <= 1) {
auctionEntity.endTime.plus(1, ChronoUnit.MINUTES)
if (ChronoUnit.SECONDS.between(Instant.now(), auctionEntity.endTime) <= 60) {
auctionEntity.endTime = Instant.now().plus(1, ChronoUnit.MINUTES)
}

return auctionMessage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.toyProject7.karrot.chatRoom.controller

import com.toyProject7.karrot.article.controller.Article
import com.toyProject7.karrot.chatRoom.service.ChatRoomService
import com.toyProject7.karrot.user.AuthUser
import com.toyProject7.karrot.user.controller.User
Expand Down Expand Up @@ -55,6 +54,6 @@ data class CreateChatRoomRequest(
)

data class ChatRoomResponse(
val article: Article,
val chatRoom: ChatRoom,
val messages: List<ChatMessage>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class ChatRoomService(
)
val messages = chatMessageEntities.map { chatMessageEntity -> ChatMessage.fromEntity(chatMessageEntity) }
return ChatRoomResponse(
article = chatRoom.article,
chatRoom = chatRoom,
messages = messages,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
package com.toyProject7.karrot.comment.controller

import com.toyProject7.karrot.comment.persistence.CommentRepository
import com.toyProject7.karrot.comment.service.CommentService
import com.toyProject7.karrot.feed.controller.FeedPreview
import com.toyProject7.karrot.feed.persistence.FeedEntity
import com.toyProject7.karrot.user.AuthUser
import com.toyProject7.karrot.user.controller.User
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
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.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
class CommentController(
private val commentService: CommentService,
private val commentRepository: CommentRepository,
) {
@PostMapping("/comment/post/{feedId}")
fun postComment(
Expand Down Expand Up @@ -69,19 +63,6 @@ class CommentController(
commentService.unlikeComment(commentId, user.id)
return ResponseEntity.ok("Unliked Successfully")
}

@GetMapping("/myfeed/comment")
fun getFeedsByUserComments(
@RequestParam("feedId") feedId: Long,
@AuthUser user: User,
): ResponseEntity<List<FeedPreview>> {
val feeds: List<FeedEntity> = commentService.getFeedsByUserComments(feedId, user.id)
val response =
feeds.map { feed ->
FeedPreview.fromEntity(feed)
}
return ResponseEntity.ok(response)
}
}

data class CommentRequest(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
package com.toyProject7.karrot.comment.persistence

import com.toyProject7.karrot.feed.persistence.FeedEntity
import com.toyProject7.karrot.user.persistence.UserEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface CommentRepository : JpaRepository<CommentEntity, Long> {
@Query(
"""
SELECT DISTINCT c.feed
FROM comments c
WHERE c.user = :user AND c.feed.id < :feedId
ORDER BY c.feed.id DESC
""",
)
fun findFeedsByUserComments(
@Param("user") user: UserEntity,
@Param("feedId") feedId: Long,
): List<FeedEntity>
fun findByUserId(userId: String): List<CommentEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.toyProject7.karrot.comment.persistence.CommentEntity
import com.toyProject7.karrot.comment.persistence.CommentLikesEntity
import com.toyProject7.karrot.comment.persistence.CommentLikesRepository
import com.toyProject7.karrot.comment.persistence.CommentRepository
import com.toyProject7.karrot.feed.persistence.FeedEntity
import com.toyProject7.karrot.feed.service.FeedService
import com.toyProject7.karrot.user.service.UserService
import org.springframework.data.repository.findByIdOrNull
Expand Down Expand Up @@ -106,13 +105,8 @@ class CommentService(
}

@Transactional
fun getFeedsByUserComments(
feedId: Long,
id: String,
): List<FeedEntity> {
val user = userService.getUserEntityById(id)
val feeds = commentRepository.findFeedsByUserComments(user, feedId)
return feeds
fun getCommentsByUser(id: String): List<CommentEntity> {
return commentRepository.findByUserId(id)
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ class FeedController(
}
return ResponseEntity.ok(response)
}

@GetMapping("/myfeed/comment")
fun getFeedsThatUserComments(
@RequestParam("feedId") feedId: Long,
@AuthUser user: User,
): ResponseEntity<List<FeedPreview>> {
val feeds: List<FeedEntity> = feedService.getFeedsThatUserComments(user.id, feedId)
val response =
feeds.map { feed ->
FeedPreview.fromEntity(feed)
}
return ResponseEntity.ok(response)
}
}

data class PostFeedRequest(
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/com/toyProject7/karrot/feed/service/FeedService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ class FeedService(
return feeds
}

@Transactional
fun getFeedsThatUserComments(
id: String,
feedId: Long,
): List<FeedEntity> {
val comments: List<CommentEntity> = commentService.getCommentsByUser(id)
val feeds: List<FeedEntity> =
comments
.map { it.feed }
.filter { it.id!! < feedId }
.distinctBy { it.id }
.sortedByDescending { it.id }
if (feeds.size < 10) return feeds
return feeds.subList(0, 10)
}

@Transactional
fun getFeedEntityById(feedId: Long): FeedEntity {
return feedRepository.findByIdOrNull(feedId) ?: throw FeedNotFoundException()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ class ProfileNotFoundException : ProfileException(
httpStatusCode = HttpStatus.NOT_FOUND,
msg = "Profile not found",
)

class ProfileEditNicknameConflictException : ProfileException(
errorCode = 0,
httpStatusCode = HttpStatus.CONFLICT,
msg = "Nickname conflict",
)
Loading

0 comments on commit 0a3593e

Please sign in to comment.