Skip to content

Commit 68e471d

Browse files
loseminhonamgigun
andauthored
Feat, refactor : 파일 업로드 기능 수정, 방명록 기능 구현 (#296) (#307)
* refactor: 스더티룸 권한에 대한 로직 개선 * fix: ci에서 통과 못한 테스트코드 수정 * fix:rest api와 웹소켓 중간 경로 통합 * fix:rest api와 웹소켓 중간 경로 통합 * fix: 에러 확인을 위한 통합테스트 추가, Room.create()메서드 수정 * refactor, feat : 조회 분할 * refactor: redis 로직 최적화 및 중복 검증 로직 제거 * fix: 에러 번호 수정 * feat: 스터디룸 방 비밀번호 변경 및 삭제 기능 구현 * fix:app-dev 제거 * feat: 웹소켓 기반 소극적 하트비트 * feat: 스터디룸 썸네일 기능 추가 및 webrtc 설정 변경에서 주석처리 * fix:소극적 하트비트 사용 주석처리 * Feat: 스터디 룸 내에 고양이 아바타 시스템과 프로필 이미지 url 연동 * fix: 기존 작성되어있던 test 코드 수정 * test: 아바타 테스트 코드 완료 * refactor: 프론트엔드 요청 사항에 따른 스터디룸 조회 마스킹 제거 * feat: 스터디룸 방 초대 코드 시스템 * Infra: main branch 로컬 환경과 운영 환경 동기화 * Infra: docker-compose 파일 수정 - Redis 버전 업그레이드 기존: 6.2 -> 변경: 7.0 * Fix: 백엔드 CD 파일 수정 - 자동화 시, 잘못된 도메인으로 호스트 ID 검증하는 오류 해결 * Infra: EC2 환경변수 수정 - 잘못 표기한 도메인 네임 변경 * Chore: CD 파일 수정 - Github Actions commandLine 인식 문제로 인해 set -Eeuo pipefail 줄바꿈 * Chore: 백엔드 CD 파일 수정 - 인스턴스 ID 체크 삭제 * Infra: 백엔드 CD 파일 수정 - .env 파일 추가시, $DOT_ENV_PROD -> $DOT_ENV 로 변경 * Infra: 도커 컴포즈 수정 - mysql 사용자 정보 변경 * Infra: 운영환경 설정 - application-prod.yml 과 application.yml 동기화 * Fix: SecurityConfig 수정 - H2 DB 허용 X * test,fix: 방 초대에 대한 테스트 코드 작성 및 에러 수정 * fix: 스터디룸 파일 업로드 맵핑 형식으로 변환 * fix: 병합충돌 제어 수정 * fix: 병합충돌 제어 * fix: 스터디 룸 내 프론트엔드 요구 사항 및 오류사항 수정 * feat: 방 즐겨찾기, 방 공지사항 구현 * fix: mockbean 수정 * fix: 테스트에서 빠진 비로그인 사용자 추가 * hotfix: 누락된 사용자 추방에 대한 컨트롤러 추가 * hotfix: VISITOR도 추방 가능하도록 수정 * fix: 누락된 테스트코드 추가 및 테스트코드 로직 수정 * refactor: 아바타 시스템 db와 분리 및 테스트 코드 수정 * fix: 턴서버 dev에 맞춤 * hotfix: 추방 후 추방당한 유저에게 개인 메시지 전송 로직 추가 * fix: 웹소켓 메세지 전송 * fix: 병합 오류 제어 * test: 테스트코드 수정 * refactor: 스터디룸 파일 업로드 s3 + fileAttachment + Mapping 제거 방식으로 수정 * feat: 방 내 방명록 기능 추가 --------- Co-authored-by: namgigun <[email protected]>
1 parent 3ffaa7a commit 68e471d

20 files changed

+1565
-16
lines changed
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package com.back.domain.studyroom.controller;
2+
3+
import com.back.domain.studyroom.dto.*;
4+
import com.back.domain.studyroom.service.RoomGuestbookService;
5+
import com.back.global.common.dto.RsData;
6+
import com.back.global.security.user.CurrentUser;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
import jakarta.validation.Valid;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.PageRequest;
15+
import org.springframework.data.domain.Pageable;
16+
import org.springframework.http.HttpStatus;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.web.bind.annotation.*;
19+
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
/**
24+
* 방명록 관리 Controller
25+
* - 방명록 CRUD
26+
* - 이모지 반응 추가/제거
27+
* - 개인별 핀 기능
28+
*/
29+
@RestController
30+
@RequiredArgsConstructor
31+
@RequestMapping("/api/room/{roomId}/guestbook")
32+
@Tag(name = "Room Guestbook", description = "방명록 관리 API - 방명록 작성, 조회, 수정, 삭제 및 이모지 반응, 개인 핀 기능")
33+
public class RoomGuestbookController {
34+
35+
private final RoomGuestbookService guestbookService;
36+
private final CurrentUser currentUser;
37+
38+
/**
39+
* 방명록 목록 조회 (페이징)
40+
*
41+
* @param roomId 방 ID
42+
* @param page 페이지 번호 (0부터 시작)
43+
* @param size 페이지 크기
44+
* @return 방명록 목록
45+
*/
46+
@GetMapping
47+
@Operation(
48+
summary = "방명록 목록 조회",
49+
description = "특정 방의 방명록 목록을 조회합니다. 로그인한 사용자가 핀한 방명록이 최상단에 표시됩니다. 페이징을 지원하며, 각 방명록의 이모지 반응과 핀 상태가 포함됩니다."
50+
)
51+
@ApiResponses({
52+
@ApiResponse(responseCode = "200", description = "방명록 목록 조회 성공"),
53+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방")
54+
})
55+
public ResponseEntity<RsData<Map<String, Object>>> getGuestbooks(
56+
@PathVariable Long roomId,
57+
@RequestParam(defaultValue = "0") int page,
58+
@RequestParam(defaultValue = "20") int size) {
59+
60+
Long currentUserId = currentUser.getUserIdOrNull();
61+
Pageable pageable = PageRequest.of(page, size);
62+
63+
Page<GuestbookResponse> guestbooks = guestbookService.getGuestbooks(roomId, currentUserId, pageable);
64+
65+
Map<String, Object> response = new HashMap<>();
66+
response.put("guestbooks", guestbooks.getContent());
67+
response.put("totalPages", guestbooks.getTotalPages());
68+
response.put("totalElements", guestbooks.getTotalElements());
69+
response.put("currentPage", guestbooks.getNumber());
70+
response.put("pageSize", guestbooks.getSize());
71+
72+
return ResponseEntity
73+
.status(HttpStatus.OK)
74+
.body(RsData.success("방명록 목록 조회 성공", response));
75+
}
76+
77+
/**
78+
* 방명록 단건 조회
79+
*
80+
* @param roomId 방 ID
81+
* @param guestbookId 방명록 ID
82+
* @return 방명록 상세 정보
83+
*/
84+
@GetMapping("/{guestbookId}")
85+
@Operation(
86+
summary = "방명록 단건 조회",
87+
description = "특정 방명록의 상세 정보를 조회합니다. 작성자 정보, 내용, 이모지 반응, 핀 상태 등이 포함됩니다."
88+
)
89+
@ApiResponses({
90+
@ApiResponse(responseCode = "200", description = "방명록 조회 성공"),
91+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방명록")
92+
})
93+
public ResponseEntity<RsData<GuestbookResponse>> getGuestbook(
94+
@PathVariable Long roomId,
95+
@PathVariable Long guestbookId) {
96+
97+
Long currentUserId = currentUser.getUserIdOrNull();
98+
GuestbookResponse guestbook = guestbookService.getGuestbook(guestbookId, currentUserId);
99+
100+
return ResponseEntity
101+
.status(HttpStatus.OK)
102+
.body(RsData.success("방명록 조회 성공", guestbook));
103+
}
104+
105+
/**
106+
* 방명록 작성
107+
*
108+
* @param roomId 방 ID
109+
* @param request 방명록 내용
110+
* @return 생성된 방명록
111+
*/
112+
@PostMapping
113+
@Operation(
114+
summary = "방명록 작성",
115+
description = "특정 방에 방명록을 작성합니다. 방을 방문한 사용자가 메시지를 남길 수 있으며, 최대 500자까지 작성 가능합니다."
116+
)
117+
@ApiResponses({
118+
@ApiResponse(responseCode = "201", description = "방명록 작성 성공"),
119+
@ApiResponse(responseCode = "400", description = "잘못된 요청 (내용 누락 또는 500자 초과)"),
120+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방"),
121+
@ApiResponse(responseCode = "401", description = "인증 실패")
122+
})
123+
public ResponseEntity<RsData<GuestbookResponse>> createGuestbook(
124+
@PathVariable Long roomId,
125+
@RequestBody @Valid CreateGuestbookRequest request) {
126+
127+
Long userId = currentUser.getUserId();
128+
GuestbookResponse guestbook = guestbookService.createGuestbook(roomId, request.getContent(), userId);
129+
130+
return ResponseEntity
131+
.status(HttpStatus.CREATED)
132+
.body(RsData.success("방명록 작성 성공", guestbook));
133+
}
134+
135+
/**
136+
* 방명록 수정 (작성자만 가능)
137+
*
138+
* @param roomId 방 ID
139+
* @param guestbookId 방명록 ID
140+
* @param request 수정할 내용
141+
* @return 수정된 방명록
142+
*/
143+
@PutMapping("/{guestbookId}")
144+
@Operation(
145+
summary = "방명록 수정",
146+
description = "작성한 방명록의 내용을 수정합니다. 작성자 본인만 수정할 수 있습니다."
147+
)
148+
@ApiResponses({
149+
@ApiResponse(responseCode = "200", description = "방명록 수정 성공"),
150+
@ApiResponse(responseCode = "400", description = "잘못된 요청 (내용 누락 또는 500자 초과)"),
151+
@ApiResponse(responseCode = "403", description = "권한 없음 (작성자가 아님)"),
152+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방명록"),
153+
@ApiResponse(responseCode = "401", description = "인증 실패")
154+
})
155+
public ResponseEntity<RsData<GuestbookResponse>> updateGuestbook(
156+
@PathVariable Long roomId,
157+
@PathVariable Long guestbookId,
158+
@RequestBody @Valid UpdateGuestbookRequest request) {
159+
160+
Long userId = currentUser.getUserId();
161+
GuestbookResponse guestbook = guestbookService.updateGuestbook(guestbookId, request.getContent(), userId);
162+
163+
return ResponseEntity
164+
.status(HttpStatus.OK)
165+
.body(RsData.success("방명록 수정 성공", guestbook));
166+
}
167+
168+
/**
169+
* 방명록 삭제 (작성자만 가능)
170+
*
171+
* @param roomId 방 ID
172+
* @param guestbookId 방명록 ID
173+
* @return 성공 메시지
174+
*/
175+
@DeleteMapping("/{guestbookId}")
176+
@Operation(
177+
summary = "방명록 삭제",
178+
description = "작성한 방명록을 삭제합니다. 작성자 본인만 삭제할 수 있으며, 삭제 시 관련된 이모지 반응과 핀도 함께 삭제됩니다."
179+
)
180+
@ApiResponses({
181+
@ApiResponse(responseCode = "200", description = "방명록 삭제 성공"),
182+
@ApiResponse(responseCode = "403", description = "권한 없음 (작성자가 아님)"),
183+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방명록"),
184+
@ApiResponse(responseCode = "401", description = "인증 실패")
185+
})
186+
public ResponseEntity<RsData<Void>> deleteGuestbook(
187+
@PathVariable Long roomId,
188+
@PathVariable Long guestbookId) {
189+
190+
Long userId = currentUser.getUserId();
191+
guestbookService.deleteGuestbook(guestbookId, userId);
192+
193+
return ResponseEntity
194+
.status(HttpStatus.OK)
195+
.body(RsData.success("방명록 삭제 성공"));
196+
}
197+
198+
/**
199+
* 방명록 이모지 반응 추가/제거 (토글)
200+
* - 이미 반응한 이모지면 제거
201+
* - 반응하지 않은 이모지면 추가
202+
*
203+
* @param roomId 방 ID
204+
* @param guestbookId 방명록 ID
205+
* @param request 이모지
206+
* @return 업데이트된 방명록 (반응 포함)
207+
*/
208+
@PostMapping("/{guestbookId}/reaction")
209+
@Operation(
210+
summary = "이모지 반응 토글",
211+
description = "방명록에 이모지 반응을 추가하거나 제거합니다. 이미 해당 이모지로 반응한 경우 제거되고, 반응하지 않은 경우 추가됩니다. 한 사용자는 같은 이모지로 중복 반응할 수 없지만, 여러 종류의 이모지로 반응할 수 있습니다."
212+
)
213+
@ApiResponses({
214+
@ApiResponse(responseCode = "200", description = "이모지 반응 토글 성공"),
215+
@ApiResponse(responseCode = "400", description = "잘못된 요청 (이모지 형식 오류)"),
216+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방명록"),
217+
@ApiResponse(responseCode = "401", description = "인증 실패")
218+
})
219+
public ResponseEntity<RsData<GuestbookResponse>> toggleReaction(
220+
@PathVariable Long roomId,
221+
@PathVariable Long guestbookId,
222+
@RequestBody @Valid AddGuestbookReactionRequest request) {
223+
224+
Long userId = currentUser.getUserId();
225+
GuestbookResponse guestbook = guestbookService.toggleReaction(guestbookId, request.getEmoji(), userId);
226+
227+
return ResponseEntity
228+
.status(HttpStatus.OK)
229+
.body(RsData.success("이모지 반응 토글 성공", guestbook));
230+
}
231+
232+
/**
233+
* 방명록 핀 추가/제거 (토글)
234+
* - 이미 핀한 방명록이면 제거
235+
* - 핀하지 않은 방명록이면 추가
236+
*
237+
* @param roomId 방 ID
238+
* @param guestbookId 방명록 ID
239+
* @return 업데이트된 방명록 (핀 상태 포함)
240+
*/
241+
@PostMapping("/{guestbookId}/pin")
242+
@Operation(
243+
summary = "방명록 개인 핀 토글",
244+
description = "방명록을 개인 핀에 추가하거나 제거합니다. 핀한 방명록은 목록 조회 시 최상단에 표시됩니다. 각 사용자는 자신만의 핀 목록을 가지며, 다른 사용자에게는 영향을 주지 않습니다. (공지사항 핀과 다름)"
245+
)
246+
@ApiResponses({
247+
@ApiResponse(responseCode = "200", description = "방명록 핀 토글 성공"),
248+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방명록"),
249+
@ApiResponse(responseCode = "401", description = "인증 실패")
250+
})
251+
public ResponseEntity<RsData<GuestbookResponse>> togglePin(
252+
@PathVariable Long roomId,
253+
@PathVariable Long guestbookId) {
254+
255+
Long userId = currentUser.getUserId();
256+
GuestbookResponse guestbook = guestbookService.togglePin(guestbookId, userId);
257+
258+
return ResponseEntity
259+
.status(HttpStatus.OK)
260+
.body(RsData.success("방명록 핀 토글 성공", guestbook));
261+
}
262+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.back.domain.studyroom.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Pattern;
5+
import jakarta.validation.constraints.Size;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public class AddGuestbookReactionRequest {
14+
15+
@NotBlank(message = "이모지는 필수입니다")
16+
@Size(max = 10, message = "이모지는 10자를 초과할 수 없습니다")
17+
private String emoji;
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.back.domain.studyroom.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class CreateGuestbookRequest {
13+
14+
@NotBlank(message = "방명록 내용은 필수입니다")
15+
@Size(max = 500, message = "방명록은 500자를 초과할 수 없습니다")
16+
private String content;
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.back.domain.studyroom.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
import java.util.List;
8+
9+
/**
10+
* 방명록 이모지 반응 요약 정보
11+
* 이모지별 개수와 반응한 사용자 정보 포함
12+
*/
13+
@Getter
14+
@Builder
15+
@AllArgsConstructor
16+
public class GuestbookReactionSummary {
17+
private String emoji;
18+
private Long count;
19+
private Boolean reactedByMe; // 현재 사용자가 이 이모지로 반응했는지
20+
private List<String> recentUsers; // 최근 반응한 사용자 닉네임 (최대 3명)
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.back.domain.studyroom.dto;
2+
3+
import com.back.domain.studyroom.entity.RoomGuestbook;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.List;
10+
11+
@Getter
12+
@Builder
13+
@AllArgsConstructor
14+
public class GuestbookResponse {
15+
private Long guestbookId;
16+
private Long userId;
17+
private String nickname;
18+
private String profileImageUrl;
19+
private String content;
20+
private LocalDateTime createdAt;
21+
private LocalDateTime updatedAt;
22+
private Boolean isAuthor; // 현재 사용자가 작성자인지
23+
private Boolean isPinned; // 현재 사용자가 핀했는지
24+
private List<GuestbookReactionSummary> reactions; // 이모지 반응 요약
25+
26+
public static GuestbookResponse from(
27+
RoomGuestbook guestbook,
28+
Long currentUserId,
29+
List<GuestbookReactionSummary> reactions,
30+
boolean isPinned) {
31+
return GuestbookResponse.builder()
32+
.guestbookId(guestbook.getId())
33+
.userId(guestbook.getUser().getId())
34+
.nickname(guestbook.getUser().getNickname())
35+
.profileImageUrl(guestbook.getUser().getProfileImageUrl())
36+
.content(guestbook.getContent())
37+
.createdAt(guestbook.getCreatedAt())
38+
.updatedAt(guestbook.getUpdatedAt())
39+
.isAuthor(currentUserId != null && guestbook.isAuthor(currentUserId))
40+
.isPinned(isPinned)
41+
.reactions(reactions)
42+
.build();
43+
}
44+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.back.domain.studyroom.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class UpdateGuestbookRequest {
13+
14+
@NotBlank(message = "방명록 내용은 필수입니다")
15+
@Size(max = 500, message = "방명록은 500자를 초과할 수 없습니다")
16+
private String content;
17+
}

0 commit comments

Comments
 (0)