Skip to content

Commit 83f1d09

Browse files
authored
Merge branch 'dev' into Feat/289
2 parents c458f46 + 3bff555 commit 83f1d09

File tree

11 files changed

+437
-52
lines changed

11 files changed

+437
-52
lines changed

src/main/java/com/back/domain/board/post/controller/docs/PostControllerDocs.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public interface PostControllerDocs {
5555
{ "id": 1, "name": "공지사항" },
5656
{ "id": 2, "name": "자유게시판" }
5757
],
58+
"images": [
59+
{ "id": 11, "url": "https://example.com/image1.png" },
60+
{ "id": 12, "url": "https://example.com/image2.png" }
61+
],
5862
"createdAt": "2025-09-22T10:30:00",
5963
"updatedAt": "2025-09-22T10:30:00"
6064
}
@@ -112,7 +116,7 @@ public interface PostControllerDocs {
112116
),
113117
@ApiResponse(
114118
responseCode = "404",
115-
description = "존재하지 않는 사용자 또는 카테고리",
119+
description = "존재하지 않는 리소스 (사용자, 카테고리, 파일)",
116120
content = @Content(
117121
mediaType = "application/json",
118122
examples = {
@@ -131,6 +135,14 @@ public interface PostControllerDocs {
131135
"message": "존재하지 않는 카테고리입니다.",
132136
"data": null
133137
}
138+
"""),
139+
@ExampleObject(name = "존재하지 않는 파일(이미지)", value = """
140+
{
141+
"success": false,
142+
"code": "FILE_004",
143+
"message": "파일 정보를 찾을 수 없습니다.",
144+
"data": null
145+
}
134146
""")
135147
}
136148
)
@@ -252,14 +264,22 @@ ResponseEntity<RsData<PageResponse<PostListResponse>>> getPosts(
252264
"message": "게시글이 조회되었습니다.",
253265
"data": {
254266
"postId": 101,
255-
"author": { "id": 5, "nickname": "홍길동", "profileImageUrl": null },
267+
"author": {
268+
"id": 5,
269+
"nickname": "홍길동",
270+
"profileImageUrl": null
271+
},
256272
"title": "첫 번째 게시글",
257273
"content": "안녕하세요, 첫 글입니다!",
258274
"thumbnailUrl": null,
259275
"categories": [
260276
{ "id": 1, "name": "공지사항" },
261277
{ "id": 2, "name": "자유게시판" }
262278
],
279+
"images": [
280+
{ "id": 11, "url": "https://example.com/image1.png" },
281+
{ "id": 12, "url": "https://example.com/image2.png" }
282+
],
263283
"likeCount": 10,
264284
"bookmarkCount": 2,
265285
"commentCount": 3,
@@ -337,8 +357,12 @@ ResponseEntity<RsData<PostDetailResponse>> getPost(
337357
{ "id": 1, "name": "공지사항" },
338358
{ "id": 2, "name": "자유게시판" }
339359
],
360+
"images": [
361+
{ "id": 11, "url": "https://example.com/image1.png" },
362+
{ "id": 12, "url": "https://example.com/image2.png" }
363+
],
340364
"createdAt": "2025-09-22T10:30:00",
341-
"updatedAt": "2025-09-22T10:30:00"
365+
"updatedAt": "2025-09-22T10:45:00"
342366
}
343367
}
344368
""")
@@ -409,7 +433,7 @@ ResponseEntity<RsData<PostDetailResponse>> getPost(
409433
),
410434
@ApiResponse(
411435
responseCode = "404",
412-
description = "존재하지 않는 사용자/게시글/카테고리",
436+
description = "존재하지 않는 리소스 (사용자, 게시글, 카테고리, 파일)",
413437
content = @Content(
414438
mediaType = "application/json",
415439
examples = {
@@ -436,6 +460,14 @@ ResponseEntity<RsData<PostDetailResponse>> getPost(
436460
"message": "존재하지 않는 카테고리입니다.",
437461
"data": null
438462
}
463+
"""),
464+
@ExampleObject(name = "존재하지 않는 파일(이미지)", value = """
465+
{
466+
"success": false,
467+
"code": "FILE_004",
468+
"message": "파일 정보를 찾을 수 없습니다.",
469+
"data": null
470+
}
439471
""")
440472
}
441473
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.board.post.dto;
2+
3+
import com.back.domain.file.entity.FileAttachment;
4+
5+
/**
6+
* 이미지 응답 DTO
7+
*
8+
* @param id 이미지 ID
9+
* @param url 이미지 URL
10+
*/
11+
public record ImageResponse(
12+
Long id,
13+
String url
14+
) {
15+
public static ImageResponse from(FileAttachment fileAttachment) {
16+
return new ImageResponse(
17+
fileAttachment.getId(),
18+
fileAttachment.getPublicURL()
19+
);
20+
}
21+
}

src/main/java/com/back/domain/board/post/dto/PostDetailResponse.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.back.domain.board.common.dto.AuthorResponse;
44
import com.back.domain.board.post.entity.Post;
5+
import com.back.domain.file.entity.FileAttachment;
56

67
import java.time.LocalDateTime;
78
import java.util.List;
@@ -15,6 +16,7 @@
1516
* @param content 게시글 내용
1617
* @param thumbnailUrl 썸네일 URL
1718
* @param categories 게시글 카테고리 목록
19+
* @param images 첨부된 이미지 목록
1820
* @param likeCount 좋아요 수
1921
* @param bookmarkCount 북마크 수
2022
* @param commentCount 댓글 수
@@ -30,6 +32,7 @@ public record PostDetailResponse(
3032
String content,
3133
String thumbnailUrl,
3234
List<CategoryResponse> categories,
35+
List<ImageResponse> images,
3336
long likeCount,
3437
long bookmarkCount,
3538
long commentCount,
@@ -38,11 +41,11 @@ public record PostDetailResponse(
3841
LocalDateTime createdAt,
3942
LocalDateTime updatedAt
4043
) {
41-
public static PostDetailResponse from(Post post) {
42-
return from(post, false, false);
44+
public static PostDetailResponse from(Post post, List<FileAttachment> attachments) {
45+
return from(post, attachments, false, false);
4346
}
4447

45-
public static PostDetailResponse from(Post post, boolean likedByMe, boolean bookmarkedByMe) {
48+
public static PostDetailResponse from(Post post, List<FileAttachment> attachments, boolean likedByMe, boolean bookmarkedByMe) {
4649
return new PostDetailResponse(
4750
post.getId(),
4851
AuthorResponse.from(post.getUser()),
@@ -52,6 +55,9 @@ public static PostDetailResponse from(Post post, boolean likedByMe, boolean book
5255
post.getCategories().stream()
5356
.map(CategoryResponse::from)
5457
.toList(),
58+
attachments.stream()
59+
.map(ImageResponse::from)
60+
.toList(),
5561
post.getPostLikes().size(),
5662
post.getPostBookmarks().size(),
5763
post.getComments().size(),

src/main/java/com/back/domain/board/post/dto/PostRequest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
* @param content 게시글 내용
1212
* @param thumbnailUrl 썸네일 URL
1313
* @param categoryIds 카테고리 ID 리스트
14+
* @param imageIds 이미지 ID 리스트
1415
*/
1516
public record PostRequest(
1617
@NotBlank String title,
1718
@NotBlank String content,
1819
String thumbnailUrl,
19-
List<Long> categoryIds
20+
List<Long> categoryIds,
21+
List<Long> imageIds
2022
) {}

src/main/java/com/back/domain/board/post/dto/PostResponse.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.back.domain.board.common.dto.AuthorResponse;
44
import com.back.domain.board.post.entity.Post;
5+
import com.back.domain.file.entity.FileAttachment;
6+
57
import java.time.LocalDateTime;
68
import java.util.List;
79

@@ -14,6 +16,7 @@
1416
* @param content 게시글 내용
1517
* @param thumbnailUrl 썸네일 URL
1618
* @param categories 게시글 카테고리 목록
19+
* @param images 첨부된 이미지 목록
1720
* @param createdAt 게시글 생성 일시
1821
* @param updatedAt 게시글 수정 일시
1922
*/
@@ -24,10 +27,11 @@ public record PostResponse(
2427
String content,
2528
String thumbnailUrl,
2629
List<CategoryResponse> categories,
30+
List<ImageResponse> images,
2731
LocalDateTime createdAt,
2832
LocalDateTime updatedAt
2933
) {
30-
public static PostResponse from(Post post) {
34+
public static PostResponse from(Post post, List<FileAttachment> attachments) {
3135
return new PostResponse(
3236
post.getId(),
3337
AuthorResponse.from(post.getUser()),
@@ -37,6 +41,9 @@ public static PostResponse from(Post post) {
3741
post.getCategories().stream()
3842
.map(CategoryResponse::from)
3943
.toList(),
44+
attachments.stream()
45+
.map(ImageResponse::from)
46+
.toList(),
4047
post.getCreatedAt(),
4148
post.getUpdatedAt()
4249
);

src/main/java/com/back/domain/board/post/service/PostService.java

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
import com.back.domain.board.post.dto.PostListResponse;
88
import com.back.domain.board.post.dto.PostRequest;
99
import com.back.domain.board.post.dto.PostResponse;
10+
import com.back.domain.board.post.entity.PostCategoryMapping;
1011
import com.back.domain.board.post.repository.PostBookmarkRepository;
1112
import com.back.domain.board.post.repository.PostCategoryRepository;
1213
import com.back.domain.board.post.repository.PostLikeRepository;
1314
import com.back.domain.board.post.repository.PostRepository;
15+
import com.back.domain.file.entity.AttachmentMapping;
16+
import com.back.domain.file.entity.EntityType;
17+
import com.back.domain.file.entity.FileAttachment;
18+
import com.back.domain.file.repository.AttachmentMappingRepository;
19+
import com.back.domain.file.repository.FileAttachmentRepository;
1420
import com.back.domain.user.common.entity.User;
1521
import com.back.domain.user.common.repository.UserRepository;
1622
import com.back.global.exception.CustomException;
@@ -32,6 +38,8 @@ public class PostService {
3238
private final PostBookmarkRepository postBookmarkRepository;
3339
private final UserRepository userRepository;
3440
private final PostCategoryRepository postCategoryRepository;
41+
private final FileAttachmentRepository fileAttachmentRepository;
42+
private final AttachmentMappingRepository attachmentMappingRepository;
3543

3644
/**
3745
* 게시글 생성 서비스
@@ -47,15 +55,24 @@ public PostResponse createPost(PostRequest request, Long userId) {
4755

4856
// Post 생성
4957
Post post = new Post(user, request.title(), request.content(), request.thumbnailUrl());
50-
Post saved = postRepository.save(post);
58+
postRepository.save(post);
5159

5260
// Category 매핑
5361
if (request.categoryIds() != null) {
5462
List<PostCategory> categories = validateAndFindCategories(request.categoryIds());
55-
saved.updateCategories(categories);
63+
categories.forEach(category -> new PostCategoryMapping(post, category));
5664
}
5765

58-
return PostResponse.from(saved);
66+
// AttachmentMapping 매핑
67+
List<FileAttachment> attachments = List.of();
68+
if (request.imageIds() != null && !request.imageIds().isEmpty()) {
69+
attachments = validateAndFindAttachments(request.imageIds());
70+
for (FileAttachment attachment : attachments) {
71+
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()));
72+
}
73+
}
74+
75+
return PostResponse.from(post, attachments);
5976
}
6077

6178
/**
@@ -86,15 +103,22 @@ public PostDetailResponse getPost(Long postId, Long userId) {
86103
Post post = postRepository.findById(postId)
87104
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
88105

106+
// 첨부파일 조회
107+
List<FileAttachment> attachments = attachmentMappingRepository
108+
.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId())
109+
.stream()
110+
.map(AttachmentMapping::getFileAttachment)
111+
.toList();
112+
89113
// 로그인 사용자 추가 데이터 설정 (좋아요, 북마크 여부)
90114
if (userId != null) {
91115
boolean likedByMe = postLikeRepository.existsByUserIdAndPostId(userId, post.getId());
92116
boolean bookmarkedByMe = postBookmarkRepository.existsByUserIdAndPostId(userId, post.getId());
93-
return PostDetailResponse.from(post, likedByMe, bookmarkedByMe);
117+
return PostDetailResponse.from(post, attachments, likedByMe, bookmarkedByMe);
94118
}
95119

96120
// 비로그인 사용자는 기본 응답 반환
97-
return PostDetailResponse.from(post);
121+
return PostDetailResponse.from(post, attachments);
98122
}
99123

100124
/**
@@ -126,7 +150,17 @@ public PostResponse updatePost(Long postId, PostRequest request, Long userId) {
126150
List<PostCategory> categories = validateAndFindCategories(request.categoryIds());
127151
post.updateCategories(categories);
128152

129-
return PostResponse.from(post);
153+
// AttachmentMapping 매핑
154+
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
155+
List<FileAttachment> attachments = List.of();
156+
if (request.imageIds() != null && !request.imageIds().isEmpty()) {
157+
attachments = validateAndFindAttachments(request.imageIds());
158+
attachments.forEach(attachment ->
159+
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
160+
);
161+
}
162+
163+
return PostResponse.from(post, attachments);
130164
}
131165

132166
/**
@@ -149,6 +183,9 @@ public void deletePost(Long postId, Long userId) {
149183
throw new CustomException(ErrorCode.POST_NO_PERMISSION);
150184
}
151185

186+
// AttachmentMapping 매핑 제거
187+
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
188+
152189
// Post 삭제
153190
post.remove();
154191
postRepository.delete(post);
@@ -158,11 +195,19 @@ public void deletePost(Long postId, Long userId) {
158195
* 카테고리 ID 유효성 검증 및 조회
159196
*/
160197
private List<PostCategory> validateAndFindCategories(List<Long> categoryIds) {
161-
List<PostCategory> categories = postCategoryRepository.findAllById(categoryIds);
198+
return categoryIds.stream()
199+
.map(id -> postCategoryRepository.findById(id)
200+
.orElseThrow(() -> new CustomException(ErrorCode.CATEGORY_NOT_FOUND)))
201+
.toList();
202+
}
162203

163-
if (categories.size() != categoryIds.size()) {
164-
throw new CustomException(ErrorCode.CATEGORY_NOT_FOUND);
165-
}
166-
return categories;
204+
/**
205+
* 첨부 파일 ID 유효성 검증 및 조회
206+
*/
207+
private List<FileAttachment> validateAndFindAttachments(List<Long> imageIds) {
208+
return imageIds.stream()
209+
.map(id -> fileAttachmentRepository.findById(id)
210+
.orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND)))
211+
.toList();
167212
}
168213
}

src/main/java/com/back/domain/file/entity/AttachmentMapping.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
@Getter
1010
@NoArgsConstructor
1111
public class AttachmentMapping extends BaseEntity {
12-
@ManyToOne(fetch = FetchType.LAZY)
13-
@JoinColumn(name = "attachment_id")
12+
@OneToOne(fetch = FetchType.LAZY, mappedBy = "attachmentMapping", cascade = CascadeType.ALL, orphanRemoval = true)
1413
private FileAttachment fileAttachment;
1514

1615
@Enumerated(EnumType.STRING)

src/main/java/com/back/domain/file/entity/FileAttachment.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ public class FileAttachment extends BaseEntity {
2929
@JoinColumn(name = "uploaded_by")
3030
private User user;
3131

32-
@OneToMany(mappedBy = "fileAttachment", cascade = CascadeType.ALL, orphanRemoval = true)
33-
private List<AttachmentMapping> attachmentMappings = new ArrayList<>();
32+
@OneToOne(fetch = FetchType.LAZY)
33+
@JoinColumn(name = "attachmentMapping_id")
34+
private AttachmentMapping attachmentMapping;
3435

3536
public FileAttachment(
3637
String storedName,

0 commit comments

Comments
 (0)