Repository: prgrms-be-devcourse/NBE-4-5-2-Team05
Pull request #37: 김아성/feat 2 상품 게시글 및 카테고리 구현
Author: login: asungkim
Labels: name: feature
description: 새로운 기능 추가
Created: 2025-03-04T00:15:41Z | Merged: 2025-03-05T08:20:07Z

## 개요
<!---- 변경 사항 및 관련 이슈에 대해 간단하게 작성해주세요. 어떻게보다 무엇을 왜 수정했는지 설명해주세요. -->

<!---- Resolves: #2  -->

## PR 유형
어떤 변경 사항이 있나요?

- [x] 새로운 기능 추가
- [ ] 버그 수정
- [ ] 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
- [x] 코드 리팩토링(성능, 기능 메서드)
- [ ] 주석 추가 및 수정
- [ ] 문서 수정
- [ ] 테스트 추가, 테스트 리팩토링
- [ ] 빌드 부분 혹은 패키지 매니저 수정
- [ ] 파일 혹은 폴더명 수정
- [ ] 파일 혹은 폴더 삭제

## PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.

- [x] 커밋 메시지 컨벤션에 맞게 작성했습니다.  Commit message convention 참고  (Ctrl + 클릭하세요.) 
- [x] 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

## 구현 내용

### 📝 게시글 기능 (`/api/posts`)

#### ✅ 글 작성 (`POST /api/posts`)
- **ProductPostWriteReqBody 를 통해 입력받아 게시글 생성**
- **사용자가 로그인되어 있어야 작성 가능** -> 유저 추가 이후 추가할 예정
- **카테고리 ID를 받아 `ProductCategory`와 연관 관계 설정** 
- **imageUrl은 DB에서 쉼표로 저장**
- **생성된 게시글 정보 반환**

---

#### ✅ 글 목록 조회 (`GET /api/posts`)
- **페이징(`page`, `pageSize`) 및 정렬(`asc` 또는 `desc`) 지원**
- **제목(`keyword`)을 포함하는 게시글 검색 가능 (`LIKE` 검색)**
- **게시글과 연관된 `ProductCategory`, `Category`를 `@EntityGraph`를 사용해 최적화**
- **응답 데이터는 DTO(`ProductPostResponse`)로 변환하여 반환**

---

### ✅ **내가 작성한 글 조회 (`GET /api/posts/my`)**
- **로그인한 사용자의 게시글만 조회**
- **페이징(`page`, `pageSize`) 및 정렬(`sort`) 지원**
- **사용자 정보는 `rq.getUserIdentity()`를 통해 가져옴**
- **응답은 DTO(`ProductPostResponse`)로 변환하여 반환**
- **N+1 문제 방지를 위해 `@EntityGraph` 적용**
- **본인이 작성한 게시글만 필터링하여 조회**

---

#### ✅ 글 단건 조회 (`GET /api/posts/{id}`)
- **게시글 ID를 기반으로 단건 조회**
- **존재하지 않는 게시글 ID 요청 시 예외 처리**
- **`ProductCategory`를 통해 해당 게시글의 카테고리도 함께 반환**
- **DTO(`ProductPostResponse`)를 사용해 응답 구조화**

---

#### ✅ 글 수정 (`PUT /api/posts/{id}`)
- **수정할 필드만 전달 가능 (모든 필드가 필수 아님)**
- **제목, 내용, 가격, 이미지, 위치, 카테고리 변경 가능**
- **수정 권한이 있는 사용자만 수정 가능 (`canModify()` 검증)**
- **카테고리 변경 시 기존 `ProductCategory` 삭제 후 새로운 `ProductCategory` 추가**
- **수정된 데이터는 DTO(`ProductPostResponse`)로 반환**

---

#### ✅ 글 삭제 (`DELETE /api/posts/{id}`)
- **게시글 ID를 기반으로 삭제**
- **삭제 권한이 있는 사용자만 삭제 가능 (`canModify()` 검증)**
- **삭제 성공 시 응답 메시지 반환**

---




Diff:
diff --git a/backend/.gitignore b/backend/.gitignore
index 6e8f0306..7590b215 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -60,6 +60,34 @@ log/
 debug.log
 error.log
 
+
+### custom ###
+gradlew
+db_dev.mv.db
+db_dev.trace.db
+/src/main/resources/application.yml
+/src/main/resources/application-dev.yml
+/src/main/resources/application-prod.yml
+/src/main/resources/application-local.yml
+
+### 🚀 데이터베이스 관련 ###
+*.db
+*.sqlite
+*.s3db
+*.h2.db
+*.mv.db  # H2 데이터베이스 무시
+*.trace.db
+
+### 📝 로그 파일 ###
+logs/
+*.log
+*.log.*
+log/
+debug.log
+error.log
+
+=======
+
 ### 🌍 OS 관련 불필요한 파일 ###
 .DS_Store
 Thumbs.db
@@ -76,3 +104,4 @@ db_dev.mv.db  # H2 데이터베이스 파일도 무시
 gradle
 src/main/resources/application*.yml
 src/test/resources/application*.yml
+
diff --git a/backend/build.gradle b/backend/build.gradle
index bbd6738c..f1160621 100644
--- a/backend/build.gradle
+++ b/backend/build.gradle
@@ -36,6 +36,7 @@ dependencies {
 	testImplementation 'org.springframework.boot:spring-boot-starter-test'
 	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
 	implementation("io.jsonwebtoken:jjwt-api:0.12.6")
 	runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
 	runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/controller/CategoryController.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/controller/CategoryController.java
similarity index 79%
rename from backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/controller/CategoryController.java
rename to backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/controller/CategoryController.java
index fb90a105..b44a27ee 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/controller/CategoryController.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/controller/CategoryController.java
@@ -1,7 +1,7 @@
-package com.NBE_4_5_2.Team5.domain.category.controller;
+package com.NBE_4_5_2.Team5.domain.post.category.controller;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
-import com.NBE_4_5_2.Team5.domain.category.service.CategoryService;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.service.CategoryService;
 import com.NBE_4_5_2.Team5.global.dto.RsData;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/entity/Category.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/entity/Category.java
similarity index 85%
rename from backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/entity/Category.java
rename to backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/entity/Category.java
index 315eeb79..2c22eb28 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/entity/Category.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/entity/Category.java
@@ -1,4 +1,4 @@
-package com.NBE_4_5_2.Team5.domain.category.entity;
+package com.NBE_4_5_2.Team5.domain.post.category.entity;
 
 import jakarta.persistence.*;
 import lombok.*;
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/repository/CategoryRepository.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/repository/CategoryRepository.java
similarity index 61%
rename from backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/repository/CategoryRepository.java
rename to backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/repository/CategoryRepository.java
index 3040ec7d..d369a3b3 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/repository/CategoryRepository.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/repository/CategoryRepository.java
@@ -1,6 +1,6 @@
-package com.NBE_4_5_2.Team5.domain.category.repository;
+package com.NBE_4_5_2.Team5.domain.post.category.repository;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/service/CategoryService.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/service/CategoryService.java
similarity index 62%
rename from backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/service/CategoryService.java
rename to backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/service/CategoryService.java
index 7a9e9c18..7df0a1c4 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/category/service/CategoryService.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/category/service/CategoryService.java
@@ -1,7 +1,7 @@
-package com.NBE_4_5_2.Team5.domain.category.service;
+package com.NBE_4_5_2.Team5.domain.post.category.service;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
-import com.NBE_4_5_2.Team5.domain.category.repository.CategoryRepository;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.repository.CategoryRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java
index a134be99..9f344061 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java
@@ -2,8 +2,10 @@
 
 import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostModifyForm;
 import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostWriteForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.response.PreviewPostResponse;
 import com.NBE_4_5_2.Team5.domain.post.post.dto.response.ProductPostResponse;
 import com.NBE_4_5_2.Team5.domain.post.post.service.ProductPostService;
+import com.NBE_4_5_2.Team5.domain.post.post.service.RecentlyViewedService;
 import com.NBE_4_5_2.Team5.domain.user.entity.User;
 import com.NBE_4_5_2.Team5.global.Rq;
 import com.NBE_4_5_2.Team5.global.dto.Empty;
@@ -14,7 +16,6 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
-import java.io.Reader;
 import java.util.List;
 
 
@@ -23,6 +24,7 @@
 @RequiredArgsConstructor
 public class ProductPostController {
     private final ProductPostService productPostService;
+    private final RecentlyViewedService recentlyViewedService;
     private final Rq rq;
 
     @PostMapping
@@ -40,11 +42,11 @@ public RsData<ProductPostResponse> createPost(@Valid @RequestBody ProductPostWri
 
     @GetMapping
     @Transactional(readOnly = true)
-    public RsData<PageDto<ProductPostResponse>> getPosts(@RequestParam(defaultValue = "1") int page,
+    public RsData<PageDto<PreviewPostResponse>> getPosts(@RequestParam(defaultValue = "1") int page,
                                                          @RequestParam(defaultValue = "10") int pageSize,
                                                          @RequestParam(defaultValue = "") String keyword,
                                                          @RequestParam(defaultValue = "desc") String sort) {
-        PageDto<ProductPostResponse> postPage = productPostService.getPosts(page, pageSize, keyword, sort);
+        PageDto<PreviewPostResponse> postPage = productPostService.getPosts(page, pageSize, keyword, sort);
 
         return new RsData<>(
                 "200",
@@ -55,11 +57,11 @@ public RsData<PageDto<ProductPostResponse>> getPosts(@RequestParam(defaultValue
 
     @GetMapping("/my")
     @Transactional(readOnly = true)
-    public RsData<PageDto<ProductPostResponse>> getMyPosts(@RequestParam(defaultValue = "1") int page,
+    public RsData<PageDto<PreviewPostResponse>> getMyPosts(@RequestParam(defaultValue = "1") int page,
                                                            @RequestParam(defaultValue = "10") int pageSize,
                                                            @RequestParam(defaultValue = "desc") String sort) {
         User actor = rq.getUserIdentity();
-        PageDto<ProductPostResponse> postPage = productPostService.getMyPosts(actor, page, pageSize, sort);
+        PageDto<PreviewPostResponse> postPage = productPostService.getMyPosts(actor, page, pageSize, sort);
 
         return new RsData<>(
                 "200",
@@ -70,9 +72,12 @@ public RsData<PageDto<ProductPostResponse>> getMyPosts(@RequestParam(defaultValu
 
     @GetMapping("/{id}")
     @Transactional(readOnly = true)
-    public RsData<ProductPostResponse> getPost(@PathVariable String id, Reader reader) {
+    public RsData<ProductPostResponse> getPost(@PathVariable String id) {
+        User user = rq.getUserIdentity();
         ProductPostResponse postResponse = productPostService.getPost(id);
 
+        recentlyViewedService.addViewedPost(user.getId(), id);
+
         return new RsData<>(
                 "200",
                 "게시물 조회가 완료되었습니다.",
@@ -80,6 +85,20 @@ public RsData<ProductPostResponse> getPost(@PathVariable String id, Reader reade
         );
     }
 
+    @GetMapping("/recently-viewed")
+    @Transactional(readOnly = true)
+    public RsData<List<PreviewPostResponse>> getRecentlyViewPosts() {
+
+        User user = rq.getUserIdentity();
+        List<PreviewPostResponse> recentlyViewedPosts = recentlyViewedService.getRecentlyViewedPosts(user.getId());
+
+        return new RsData<>(
+                "200",
+                "최근 본 상품 목록 조회가 완료되었습니다.",
+                recentlyViewedPosts
+        );
+    }
+
     @PutMapping("/{id}")
     @Transactional
     public RsData<ProductPostResponse> modify(
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/dto/response/PreviewPostResponse.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/dto/response/PreviewPostResponse.java
new file mode 100644
index 00000000..37b8a82b
--- /dev/null
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/dto/response/PreviewPostResponse.java
@@ -0,0 +1,40 @@
+package com.NBE_4_5_2.Team5.domain.post.post.dto.response;
+
+import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@AllArgsConstructor
+@Builder
+public class PreviewPostResponse {
+    private String id;
+    private String productName;
+    private Integer productPrice;
+    private String title;
+    private String writerId;
+    private String writerName;
+    private Float latitude;
+    private Float longitude;
+    private String thumbNail;
+    private LocalDateTime createdAt;
+
+    public static PreviewPostResponse fromEntity(ProductPost post) {
+        return PreviewPostResponse.builder()
+                .id(post.getId())
+                .productName(post.getProductName())
+                .productPrice(post.getProductPrice())
+                .title(post.getTitle())
+                .writerId(post.getWriter().getId())
+                .writerName(post.getWriter().getNickname())
+                .latitude(post.getLatitude())
+                .longitude(post.getLongitude())
+                .thumbNail(post.getImage_urls().split(",")[0])
+                .createdAt(post.getCreatedAt())
+                .build();
+    }
+
+}
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductCategory.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductCategory.java
index 64e443e1..40c05217 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductCategory.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductCategory.java
@@ -1,6 +1,6 @@
 package com.NBE_4_5_2.Team5.domain.post.post.entity;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
 import jakarta.persistence.*;
 import lombok.*;
 
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
index a7ac2444..166fe8d1 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
@@ -9,7 +9,7 @@
 import org.springframework.data.annotation.LastModifiedDate;
 import org.springframework.data.jpa.domain.support.AuditingEntityListener;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
 import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
 import com.NBE_4_5_2.Team5.domain.user.entity.User;
 import com.NBE_4_5_2.Team5.global.exception.ServiceException;
@@ -36,7 +36,6 @@
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Getter
 @Builder
-@Setter
 @EntityListeners(AuditingEntityListener.class)
 public class ProductPost {
 
@@ -44,38 +43,46 @@ public class ProductPost {
 	@Column(updatable = false, nullable = false)
 	private String id;
 
-	@Column(nullable = false)
-	private String productName;
+    @Column(nullable = false)
+    @Setter
+    private String productName;
 
-	@Column(nullable = false)
-	private Integer productPrice;
+    @Column(nullable = false)
+    @Setter
+    private Integer productPrice;
 
-	@ManyToOne(fetch = FetchType.LAZY)
-	private User buyer; // 구매자(NULL이면 아직 구매 안 된 상태)
+    @ManyToOne(fetch = FetchType.LAZY)
+    private User buyer; // 구매자(NULL이면 아직 구매 안 된 상태)
 
-	@Column(nullable = false)
-	private String title;
+    @Column(nullable = false)
+    @Setter
+    private String title;
 
-	@Column(nullable = false, columnDefinition = "TEXT")
-	private String content;
+    @Column(nullable = false, columnDefinition = "TEXT")
+    @Setter
+    private String content;
 
-	@Column(nullable = false, columnDefinition = "TEXT")
-	private String image_urls; // 쉼표가 포함된 url 문자열
+    @Column(nullable = false, columnDefinition = "TEXT")
+    @Setter
+    private String image_urls; // 쉼표가 포함된 url 문자열
 
 	@Column(nullable = false)
 	@Builder.Default
 	private Integer likedCount = 0;
 
-	@Enumerated(EnumType.STRING)
-	@Column(nullable = false)
-	@Builder.Default
-	private ProductStatus status = ProductStatus.AVAILABLE;
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    @Setter
+    @Builder.Default
+    private ProductStatus status = ProductStatus.AVAILABLE;
 
-	@Column(nullable = false)
-	private Float latitude;
+    @Column(nullable = false)
+    @Setter
+    private Float latitude;
 
-	@Column(nullable = false)
-	private Float longitude;
+    @Column(nullable = false)
+    @Setter
+    private Float longitude;
 
 	@CreatedDate
 	@Column(updatable = false)
@@ -106,16 +113,19 @@ public static ProductPost create(User writer, String productName, Integer produc
 			.build();
 	}
 
-	// 구매 처리 메서드
-	public void setBuyer(User buyer) {
-		this.buyer = buyer;
-		this.status = ProductStatus.PURCHASED;
-	}
+    // 구매 처리 메서드
+    public void setBuyer(User buyer) {
+        this.buyer = buyer;
+        this.status = ProductStatus.PURCHASED;
+    }
 
-	public void addCategories(List<Category> categories) {
-		List<ProductCategory> productCategories = categories.stream()
-			.map(category -> ProductCategory.builder().productPost(this).category(category).build())
-			.toList();
+    public void addCategories(List<Category> categories) {
+        List<ProductCategory> productCategories = categories.stream()
+                .map(category -> ProductCategory.builder()
+                        .productPost(this)
+                        .category(category)
+                        .build())
+                .toList();
 
 		this.productCategories.addAll(productCategories);
 	}
@@ -151,4 +161,4 @@ public boolean isAvailable() {
 	public void updateStatus(ProductStatus status) {
 		this.status = status;
 	}
-}
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/repository/ProductPostRepository.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/repository/ProductPostRepository.java
index 4409f7b2..b6411777 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/repository/ProductPostRepository.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/repository/ProductPostRepository.java
@@ -1,8 +1,7 @@
 package com.NBE_4_5_2.Team5.domain.post.post.repository;
 
-import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
-import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
-import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import java.util.List;
+
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.EntityGraph;
@@ -11,7 +10,9 @@
 import org.springframework.lang.NonNull;
 import org.springframework.stereotype.Repository;
 
-import java.util.List;
+import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
 
 @Repository
 public interface ProductPostRepository extends JpaRepository<ProductPost, String> {
@@ -37,4 +38,6 @@ public interface ProductPostRepository extends JpaRepository<ProductPost, String
 
     @EntityGraph(attributePaths = {"productCategories.category"})
     List<ProductPost> findByBuyer(User buyer);
+
+    List<ProductPost> findByIdIn(List<String> postIds);
 }
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/ProductPostService.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/ProductPostService.java
index cd0f2a1a..acb43c71 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/ProductPostService.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/ProductPostService.java
@@ -1,9 +1,10 @@
 package com.NBE_4_5_2.Team5.domain.post.post.service;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
-import com.NBE_4_5_2.Team5.domain.category.repository.CategoryRepository;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.repository.CategoryRepository;
 import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostModifyForm;
 import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostWriteForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.response.PreviewPostResponse;
 import com.NBE_4_5_2.Team5.domain.post.post.dto.response.ProductPostResponse;
 import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductCategory;
 import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
@@ -61,7 +62,7 @@ public ProductPostResponse write(User actor, ProductPostWriteForm body) {
     }
 
 
-    public PageDto<ProductPostResponse> getPosts(int page, int pageSize, String keyword, String sort) {
+    public PageDto<PreviewPostResponse> getPosts(int page, int pageSize, String keyword, String sort) {
         Sort.Direction sortDirection = sort.equalsIgnoreCase("asc") ? Sort.Direction.ASC : Sort.Direction.DESC;
         Pageable pageable = PageRequest.of(page - 1, pageSize,
                 Sort.by(sortDirection, "createdAt"));
@@ -75,19 +76,19 @@ public PageDto<ProductPostResponse> getPosts(int page, int pageSize, String keyw
             postpage = productPostRepository.findByTitleLike(likeKeyword, pageable);
         }
 
-        Page<ProductPostResponse> mappedPosts = postpage.map(ProductPostResponse::fromEntity);
+        Page<PreviewPostResponse> mappedPosts = postpage.map(PreviewPostResponse::fromEntity);
 
         return new PageDto<>(mappedPosts);
     }
 
-    public PageDto<ProductPostResponse> getMyPosts(User actor, int page, int pageSize, String sort) {
+    public PageDto<PreviewPostResponse> getMyPosts(User actor, int page, int pageSize, String sort) {
         Sort.Direction sortDirection = sort.equalsIgnoreCase("asc") ? Sort.Direction.ASC : Sort.Direction.DESC;
         Pageable pageable = PageRequest.of(page - 1, pageSize,
                 Sort.by(sortDirection, "createdAt"));
 
         Page<ProductPost> postPage = productPostRepository.findByWriter(actor, pageable);
 
-        Page<ProductPostResponse> mappedMyPosts = postPage.map(ProductPostResponse::fromEntity);
+        Page<PreviewPostResponse> mappedMyPosts = postPage.map(PreviewPostResponse::fromEntity);
 
         return new PageDto<>(mappedMyPosts);
     }
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/RecentlyViewedService.java b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/RecentlyViewedService.java
new file mode 100644
index 00000000..73042186
--- /dev/null
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/service/RecentlyViewedService.java
@@ -0,0 +1,46 @@
+package com.NBE_4_5_2.Team5.domain.post.post.service;
+
+import com.NBE_4_5_2.Team5.domain.post.post.dto.response.PreviewPostResponse;
+import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
+import com.NBE_4_5_2.Team5.domain.post.post.repository.ProductPostRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class RecentlyViewedService {
+    private final String RECENTLY_VIEW_KEY = "recentlyViewed:";
+    private final RedisTemplate<String, String> redisTemplate;
+    private final ProductPostRepository productPostRepository;
+
+    // 최신순 -> 큐
+    public void addViewedPost(String userId, String postId) {
+        String key = RECENTLY_VIEW_KEY + userId;
+        ListOperations<String, String> listOps = redisTemplate.opsForList();
+
+        listOps.remove(key, 1, postId);
+        listOps.leftPush(key, postId);
+
+        System.out.println(listOps.getFirst(key));
+
+        if (listOps.size(key) > 10) {
+            listOps.rightPop(key);
+        }
+    }
+
+    public List<PreviewPostResponse> getRecentlyViewedPosts(String userId) {
+        String key = RECENTLY_VIEW_KEY + userId;
+        List<String> postIds = Optional.ofNullable(redisTemplate.opsForList().range(key, 0, -1))
+                .orElse(Collections.emptyList());
+
+        List<ProductPost> posts = productPostRepository.findByIdIn(postIds);
+
+        return posts.stream().map(PreviewPostResponse::fromEntity).toList();
+    }
+}
diff --git a/backend/src/main/java/com/NBE_4_5_2/Team5/global/init/BaseInitData.java b/backend/src/main/java/com/NBE_4_5_2/Team5/global/init/BaseInitData.java
index 20276c95..b86f9a71 100644
--- a/backend/src/main/java/com/NBE_4_5_2/Team5/global/init/BaseInitData.java
+++ b/backend/src/main/java/com/NBE_4_5_2/Team5/global/init/BaseInitData.java
@@ -10,8 +10,8 @@
 import org.springframework.context.annotation.Lazy;
 import org.springframework.core.annotation.Order;
 
-import com.NBE_4_5_2.Team5.domain.category.entity.Category;
-import com.NBE_4_5_2.Team5.domain.category.repository.CategoryRepository;
+import com.NBE_4_5_2.Team5.domain.post.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.category.repository.CategoryRepository;
 import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductCategory;
 import com.NBE_4_5_2.Team5.domain.post.post.entity.ProductPost;
 import com.NBE_4_5_2.Team5.domain.post.post.repository.ProductCategoryRepository;


Reviews:
user: login: sojunsik
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:23:27Z
user: login: asungkim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:32:35Z
user: login: sojunsik
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:47:44Z
user: login: omegafrog
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T00:58:52Z
user: login: omegafrog
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T01:23:17Z
user: login: asungkim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T01:28:11Z
user: login: asungkim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T01:28:41Z
user: login: omegafrog
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T02:15:54Z
user: login: asungkim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T02:40:57Z
user: login: okjunghyeon
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-05T02:49:20Z
user: login: omegafrog
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-05T05:04:18Z
user: login: omegafrog
body: 굿 좋습니다
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-05T06:33:01Z
user: login: asungkim
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T06:42:16Z
user: login: omegafrog
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-05T06:47:23Z

Review comments:
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/enums/ProductStatus.java
user: login: sojunsik
body: For_sale("판매중") 부분, 노션에서는 Available로 하기로 해서 이부분 고쳐주시면 될것같습니다.
created_at: 2025-03-04T05:07:18Z
author_association: COLLABORATOR
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/enums/ProductStatus.java
user: login: asungkim
body: 넵 확인했습니다

created_at: 2025-03-04T08:32:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
+@EntityListeners(AuditingEntityListener.class)
+public class ProductPost {
+
+    @Id
+    @Column(updatable = false, nullable = false)
+    private String id;
+
+    @Column(nullable = false)
+    private String productName;
+
+    @Column(nullable = false)
+    private Integer productPrice;
+
+    @Column(nullable = false)
+    private String title;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String content;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String image_urls; // 쉼표가 포함된 url 문자열
+
+    @Column(nullable = false)
+    @Builder.Default
+    private Integer likedCount = 0;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    @Builder.Default
+    private ProductStatus status = ProductStatus.AVAILABLE;
+
+    @Column(nullable = false)
+    private Float latitude;
+
+    @Column(nullable = false)
+    private Float longitude;
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @OneToMany(mappedBy = "productPost", cascade = CascadeType.ALL, orphanRemoval = true)
+    @Builder.Default
+    private List<ProductCategory> productCategories = new ArrayList<>();
+
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: omegafrog
body: Category로 안하고 ProductCategory로 하신 이유가 궁금합니다. ProductPost에 상품이 1개밖에 없어서 Product랑 Category를 잇는 클래스가 필요할까요?
created_at: 2025-03-05T00:54:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,111 @@
+package com.NBE_4_5_2.Team5.domain.post.post.controller;
+
+import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostModifyForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostWriteForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.response.ProductPostResponse;
+import com.NBE_4_5_2.Team5.domain.post.post.service.ProductPostService;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.Rq;
+import com.NBE_4_5_2.Team5.global.dto.Empty;
+import com.NBE_4_5_2.Team5.global.dto.PageDto;
+import com.NBE_4_5_2.Team5.global.dto.RsData;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.Reader;
+
+
+@RestController
+@RequestMapping("/api/posts")
+@RequiredArgsConstructor
+public class ProductPostController {
+    private final ProductPostService productPostService;
+    private final Rq rq;
+
+    @PostMapping
+    public RsData<ProductPostResponse> createPost(@Valid @RequestBody ProductPostWriteForm body) {
+
+        User actor = rq.getUserIdentity();
+        ProductPostResponse postResponse = productPostService.write(actor, body);
+
+        return new RsData<>(
+                "200",
+                "글 작성 성공",
+                postResponse
+        );
+    }
+
+    @GetMapping
+    @Transactional(readOnly = true)
+    public RsData<PageDto<ProductPostResponse>> getPosts(@RequestParam(defaultValue = "1") int page,
+                                                         @RequestParam(defaultValue = "10") int pageSize,
+                                                         @RequestParam(defaultValue = "") String keyword,
+                                                         @RequestParam(defaultValue = "desc") String sort) {
+        PageDto<ProductPostResponse> postPage = productPostService.getPosts(page, pageSize, keyword, sort);
+
+        return new RsData<>(
+                "200",
+                "글 목록 조회가 완료되었습니다.",
+                postPage
+        );
+    }
+
+    @GetMapping("/my")
+    @Transactional(readOnly = true)
+    public RsData<PageDto<ProductPostResponse>> getMyPosts(@RequestParam(defaultValue = "1") int page,
+                                                           @RequestParam(defaultValue = "10") int pageSize,
+                                                           @RequestParam(defaultValue = "desc") String sort) {
+        User actor = rq.getUserIdentity();
+        PageDto<ProductPostResponse> postPage = productPostService.getMyPosts(actor, page, pageSize, sort);
+
+        return new RsData<>(
+                "200",
+                "내 글 목록 조회가 완료되었습니다.",
+                postPage
+        );
+    }
+
+    @GetMapping("/{id}")
+    @Transactional(readOnly = true)
+    public RsData<ProductPostResponse> getPost(@PathVariable String id, Reader reader) {
+        ProductPostResponse postResponse = productPostService.getPost(id);
+
+        return new RsData<>(
+                "200",
+                "게시물 조회가 완료되었습니다.",
+                postResponse
+        );
+    }
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java
user: login: omegafrog
body: Reader는 사용하지 않는 것 같아 보입니다
created_at: 2025-03-05T01:22:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
+@EntityListeners(AuditingEntityListener.class)
+public class ProductPost {
+
+    @Id
+    @Column(updatable = false, nullable = false)
+    private String id;
+
+    @Column(nullable = false)
+    private String productName;
+
+    @Column(nullable = false)
+    private Integer productPrice;
+
+    @Column(nullable = false)
+    private String title;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String content;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String image_urls; // 쉼표가 포함된 url 문자열
+
+    @Column(nullable = false)
+    @Builder.Default
+    private Integer likedCount = 0;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    @Builder.Default
+    private ProductStatus status = ProductStatus.AVAILABLE;
+
+    @Column(nullable = false)
+    private Float latitude;
+
+    @Column(nullable = false)
+    private Float longitude;
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @OneToMany(mappedBy = "productPost", cascade = CascadeType.ALL, orphanRemoval = true)
+    @Builder.Default
+    private List<ProductCategory> productCategories = new ArrayList<>();
+
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: asungkim
body: 저는 한 상품에 카테고리가 여러개 들어간다고 생각했었네요.  예를 들어 아이폰이라고 하면 애플 + 스마트폰 + 전자기기 이런식으로 여러개의 카테고리를 가진다고 보았습니다.
듣고보니 태그랑 비슷해진거같아서 나중에 리펙토링한다면 태그랑 카테고리로 나누어도 좋을거같네요


created_at: 2025-03-05T01:28:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,111 @@
+package com.NBE_4_5_2.Team5.domain.post.post.controller;
+
+import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostModifyForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.request.ProductPostWriteForm;
+import com.NBE_4_5_2.Team5.domain.post.post.dto.response.ProductPostResponse;
+import com.NBE_4_5_2.Team5.domain.post.post.service.ProductPostService;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.Rq;
+import com.NBE_4_5_2.Team5.global.dto.Empty;
+import com.NBE_4_5_2.Team5.global.dto.PageDto;
+import com.NBE_4_5_2.Team5.global.dto.RsData;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.Reader;
+
+
+@RestController
+@RequestMapping("/api/posts")
+@RequiredArgsConstructor
+public class ProductPostController {
+    private final ProductPostService productPostService;
+    private final Rq rq;
+
+    @PostMapping
+    public RsData<ProductPostResponse> createPost(@Valid @RequestBody ProductPostWriteForm body) {
+
+        User actor = rq.getUserIdentity();
+        ProductPostResponse postResponse = productPostService.write(actor, body);
+
+        return new RsData<>(
+                "200",
+                "글 작성 성공",
+                postResponse
+        );
+    }
+
+    @GetMapping
+    @Transactional(readOnly = true)
+    public RsData<PageDto<ProductPostResponse>> getPosts(@RequestParam(defaultValue = "1") int page,
+                                                         @RequestParam(defaultValue = "10") int pageSize,
+                                                         @RequestParam(defaultValue = "") String keyword,
+                                                         @RequestParam(defaultValue = "desc") String sort) {
+        PageDto<ProductPostResponse> postPage = productPostService.getPosts(page, pageSize, keyword, sort);
+
+        return new RsData<>(
+                "200",
+                "글 목록 조회가 완료되었습니다.",
+                postPage
+        );
+    }
+
+    @GetMapping("/my")
+    @Transactional(readOnly = true)
+    public RsData<PageDto<ProductPostResponse>> getMyPosts(@RequestParam(defaultValue = "1") int page,
+                                                           @RequestParam(defaultValue = "10") int pageSize,
+                                                           @RequestParam(defaultValue = "desc") String sort) {
+        User actor = rq.getUserIdentity();
+        PageDto<ProductPostResponse> postPage = productPostService.getMyPosts(actor, page, pageSize, sort);
+
+        return new RsData<>(
+                "200",
+                "내 글 목록 조회가 완료되었습니다.",
+                postPage
+        );
+    }
+
+    @GetMapping("/{id}")
+    @Transactional(readOnly = true)
+    public RsData<ProductPostResponse> getPost(@PathVariable String id, Reader reader) {
+        ProductPostResponse postResponse = productPostService.getPost(id);
+
+        return new RsData<>(
+                "200",
+                "게시물 조회가 완료되었습니다.",
+                postResponse
+        );
+    }
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/controller/ProductPostController.java
user: login: asungkim
body: 넵 저도 이상한게 들어있길래 코드보다가 지웠네요.. ㅎㅎ
created_at: 2025-03-05T01:28:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
+@EntityListeners(AuditingEntityListener.class)
+public class ProductPost {
+
+    @Id
+    @Column(updatable = false, nullable = false)
+    private String id;
+
+    @Column(nullable = false)
+    private String productName;
+
+    @Column(nullable = false)
+    private Integer productPrice;
+
+    @Column(nullable = false)
+    private String title;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String content;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String image_urls; // 쉼표가 포함된 url 문자열
+
+    @Column(nullable = false)
+    @Builder.Default
+    private Integer likedCount = 0;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    @Builder.Default
+    private ProductStatus status = ProductStatus.AVAILABLE;
+
+    @Column(nullable = false)
+    private Float latitude;
+
+    @Column(nullable = false)
+    private Float longitude;
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @OneToMany(mappedBy = "productPost", cascade = CascadeType.ALL, orphanRemoval = true)
+    @Builder.Default
+    private List<ProductCategory> productCategories = new ArrayList<>();
+
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: omegafrog
body: 생각하신 대로라면 그냥 Category list로만 해도 좋지 않을까요? ProductCategory라는 클래스가 하는 일이 뭔지 이해가 잘 안돼서 물어본 질문이었습니다
created_at: 2025-03-05T02:15:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
+@EntityListeners(AuditingEntityListener.class)
+public class ProductPost {
+
+    @Id
+    @Column(updatable = false, nullable = false)
+    private String id;
+
+    @Column(nullable = false)
+    private String productName;
+
+    @Column(nullable = false)
+    private Integer productPrice;
+
+    @Column(nullable = false)
+    private String title;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String content;
+
+    @Column(nullable = false, columnDefinition = "TEXT")
+    private String image_urls; // 쉼표가 포함된 url 문자열
+
+    @Column(nullable = false)
+    @Builder.Default
+    private Integer likedCount = 0;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    @Builder.Default
+    private ProductStatus status = ProductStatus.AVAILABLE;
+
+    @Column(nullable = false)
+    private Float latitude;
+
+    @Column(nullable = false)
+    private Float longitude;
+
+    @CreatedDate
+    @Column(updatable = false)
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @OneToMany(mappedBy = "productPost", cascade = CascadeType.ALL, orphanRemoval = true)
+    @Builder.Default
+    private List<ProductCategory> productCategories = new ArrayList<>();
+
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: asungkim
body: 아 이해했습니다. ProductCategory 자체가 하는 일이 없어서 그런거군요. 
M:N 관계를 확장하는 경우를 고려하다보니 항상 1:N 으로 풀어냈어서 그렇게 한거같네요. 
created_at: 2025-03-05T02:40:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: omegafrog
body: 엔티티에 모든 setter를 열어놓는 것보다 필요한 필드만 setter를 쓰는 건 어떨까요?
created_at: 2025-03-05T05:20:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+package com.NBE_4_5_2.Team5.domain.post.post.entity;
+
+import com.NBE_4_5_2.Team5.domain.category.entity.Category;
+import com.NBE_4_5_2.Team5.domain.post.post.enums.ProductStatus;
+import com.NBE_4_5_2.Team5.domain.user.entity.User;
+import com.NBE_4_5_2.Team5.global.exception.ServiceException;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+@Builder
+@Setter
path: backend/src/main/java/com/NBE_4_5_2/Team5/domain/post/post/entity/ProductPost.java
user: login: asungkim
body: 수정해서 PR했습니다!
created_at: 2025-03-05T06:42:16Z
author_association: COLLABORATOR

PR comments:
user: login: omegafrog
created_at: 2025-03-05T05:41:18Z
body: Category를 도메인 최상단에 두지 말고 post에 넣는 건 어떨까요?
author_association: COLLABORATOR
user: login: asungkim
created_at: 2025-03-05T05:57:54Z
body: > Category를 도메인 최상단에 두지 말고 post에 넣는 건 어떨까요?

네 좋은거같아요 바꾸겠습니다
author_association: COLLABORATOR