Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e508157
refactor: 스더티룸 권한에 대한 로직 개선
loseminho Oct 2, 2025
8b766f3
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 2, 2025
ef46ed0
fix: ci에서 통과 못한 테스트코드 수정
loseminho Oct 3, 2025
23e55ea
fix:rest api와 웹소켓 중간 경로 통합
loseminho Oct 4, 2025
2979e6e
fix:rest api와 웹소켓 중간 경로 통합
loseminho Oct 4, 2025
2de8631
fix: 병합 충돌 제어
loseminho Oct 4, 2025
e576231
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 5, 2025
98c9e4c
fix: 에러 확인을 위한 통합테스트 추가, Room.create()메서드 수정
loseminho Oct 5, 2025
8cb4561
refactor, feat
loseminho Oct 5, 2025
be970fd
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 5, 2025
57c38ae
refactor: redis 로직 최적화 및 중복 검증 로직 제거
loseminho Oct 7, 2025
ad752e7
fix : 병합 충돌 제어
loseminho Oct 7, 2025
1af4d1e
fix: 에러 번호 수정
loseminho Oct 8, 2025
483a0fc
feat: 스터디룸 방 비밀번호 변경 및 삭제 기능 구현
loseminho Oct 9, 2025
89971f2
Merge remote-tracking branch 'origin/dev' into feat-215
loseminho Oct 9, 2025
77ab976
fix:app-dev 제거
loseminho Oct 9, 2025
7a17167
feat: 웹소켓 기반 소극적 하트비트
loseminho Oct 9, 2025
dd229f2
Merge remote-tracking branch 'origin/dev' into feat/217
loseminho Oct 10, 2025
cf32230
feat: 스터디룸 썸네일 기능 추가 및 webrtc 설정 변경에서 주석처리
loseminho Oct 10, 2025
be00c11
Merge branch 'feat/217' of https://github.com/prgrms-web-devcourse-fi…
loseminho Oct 10, 2025
da453f6
fix:소극적 하트비트 사용 주석처리
loseminho Oct 10, 2025
70813b6
Merge remote-tracking branch 'origin/dev' into feat/217
loseminho Oct 11, 2025
2de5447
Feat: 스터디 룸 내에 고양이 아바타 시스템과 프로필 이미지 url 연동
loseminho Oct 11, 2025
2bc4e95
Merge remote-tracking branch 'origin/dev' into feat/217
loseminho Oct 11, 2025
7d4237c
fix: 기존 작성되어있던 test 코드 수정
loseminho Oct 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.back.domain.studyroom.controller;

import com.back.domain.studyroom.dto.AvatarResponse;
import com.back.domain.studyroom.dto.UpdateAvatarRequest;
import com.back.domain.studyroom.service.AvatarService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CurrentUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* 방 아바타 관리 API 컨트롤러
* - JWT 인증 필수
* - MEMBER 등급 이상만 아바타 변경 가능
*/
@RestController
@RequestMapping("/api/rooms/{roomId}/avatars")
@RequiredArgsConstructor
@Tag(name = "Room Avatar API", description = "방 아바타 관련 API")
@SecurityRequirement(name = "Bearer Authentication")
public class RoomAvatarController {

private final AvatarService avatarService;
private final CurrentUser currentUser;

/**
* 사용 가능한 아바타 목록 조회
*/
@GetMapping
@Operation(
summary = "아바타 목록 조회",
description = "선택 가능한 아바타 목록을 조회합니다. 고양이, 강아지 등 다양한 아바타를 제공합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
public ResponseEntity<RsData<List<AvatarResponse>>> getAvatars(
@Parameter(description = "방 ID", required = true) @PathVariable Long roomId) {

List<AvatarResponse> avatars = avatarService.getAvailableAvatars();

return ResponseEntity.ok(
RsData.success("아바타 목록 조회 완료", avatars)
);
}

/**
* 내 아바타 변경 (모든 사용자 가능)
*/
@PutMapping("/me")
@Operation(
summary = "아바타 변경",
description = "방에서 사용할 아바타를 변경합니다.\n\n" +
"- VISITOR: Redis에만 저장 (퇴장 시 삭제, 재입장 시 랜덤 배정)\n" +
"- MEMBER 이상: DB에 저장 (재입장 시에도 유지)"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "변경 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 아바타 ID"),
@ApiResponse(responseCode = "404", description = "존재하지 않는 방 또는 아바타"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
public ResponseEntity<RsData<Void>> updateMyAvatar(
@Parameter(description = "방 ID", required = true) @PathVariable Long roomId,
@Valid @RequestBody UpdateAvatarRequest request) {

Long userId = currentUser.getUserId();

avatarService.updateRoomAvatar(roomId, userId, request.getAvatarId());

return ResponseEntity.ok(
RsData.success("아바타가 변경되었습니다", null)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ public ResponseEntity<RsData<Void>> activateRoom(
@GetMapping("/{roomId}/members")
@Operation(
summary = "방 멤버 목록 조회",
description = "방의 현재 온라인 멤버 목록을 조회합니다. 역할별로 정렬됩니다(방장>부방장>멤버>방문객)."
description = "방의 현재 온라인 멤버 목록을 조회합니다. 프로필 이미지와 아바타 정보를 포함. 역할별로 정렬됩니다(방장>부방장>멤버>방문객)."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
Expand All @@ -511,9 +511,8 @@ public ResponseEntity<RsData<List<RoomMemberResponse>>> getRoomMembers(

List<RoomMember> members = roomService.getRoomMembers(roomId, currentUserId);

List<RoomMemberResponse> memberList = members.stream()
.map(RoomMemberResponse::from)
.collect(Collectors.toList());
// 아바타 정보 포함하여 변환 (N+1 방지)
List<RoomMemberResponse> memberList = roomService.toRoomMemberResponseList(roomId, members);

return ResponseEntity
.status(HttpStatus.OK)
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/back/domain/studyroom/dto/AvatarResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.domain.studyroom.dto;

import com.back.domain.studyroom.entity.Avatar;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* 아바타 정보 응답 DTO
*/
@Getter
@AllArgsConstructor
public class AvatarResponse {
private Long id;
private String name;
private String imageUrl;
private String description;
private boolean isDefault;
private String category;

public static AvatarResponse from(Avatar avatar) {
return new AvatarResponse(
avatar.getId(),
avatar.getName(),
avatar.getImageUrl(),
avatar.getDescription(),
avatar.isDefault(),
avatar.getCategory()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,39 @@
public class RoomMemberResponse {
private Long userId;
private String nickname;
private String profileImageUrl;
private Long avatarId;
private String avatarImageUrl;
private RoomRole role;
private LocalDateTime joinedAt;
private LocalDateTime promotedAt;


// TODO: isOnline은 Redis에서 조회하여 추가 예정

/**
* RoomMember만으로 생성 (아바타 정보 없음)
* 기존 호환성과 간단한 조회용
*/
public static RoomMemberResponse from(RoomMember member) {
return RoomMemberResponse.builder()
.userId(member.getUser().getId())
.nickname(member.getUser().getNickname())
.profileImageUrl(member.getUser().getProfileImageUrl())
.role(member.getRole())
.joinedAt(member.getJoinedAt())
.promotedAt(member.getPromotedAt())
.build();
}

/**
* 아바타 정보를 포함된 내용으로 생성
*/
public static RoomMemberResponse of(RoomMember member, Long avatarId, String avatarImageUrl) {
return RoomMemberResponse.builder()
.userId(member.getUser().getId())
.nickname(member.getUser().getNickname())
.profileImageUrl(member.getUser().getProfileImageUrl())
.avatarId(avatarId)
.avatarImageUrl(avatarImageUrl)
.role(member.getRole())
.joinedAt(member.getJoinedAt())
.promotedAt(member.getPromotedAt())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.back.domain.studyroom.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* 아바타 변경 요청 DTO
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateAvatarRequest {

@NotNull(message = "아바타 ID는 필수입니다")
@Positive(message = "올바른 아바타 ID를 입력해주세요")
private Long avatarId;
}
36 changes: 36 additions & 0 deletions src/main/java/com/back/domain/studyroom/entity/Avatar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.back.domain.studyroom.entity;

import com.back.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

/**
* 아바타 마스터 테이블
* - 선택 가능한 모든 아바타 정보를 관리
* - 고양이, 강아지 등 다양한 아바타로 확장 가능
* - isDefault=true인 아바타(1,2,3)는 VISITOR 랜덤 배정용
*/
@Entity
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "avatars")
public class Avatar extends BaseEntity {

@Column(nullable = false, length = 50)
private String name; // "검은 고양이", "하얀 고양이", "골든 리트리버" 등등등
@Column(nullable = false, length = 500)
private String imageUrl; // CDN URL
@Column(length = 200)
private String description; // "귀여운 검은 고양이"
@Column(nullable = false)
private boolean isDefault; // 기본(랜덤) 아바타 여부
@Column(nullable = false)
private int sortOrder; // 표시 순서 (1, 2, 3...)
@Column(length = 50)
private String category; // "CAT", "DOG", "ETC" 등 (추후 확장 가능을 위해 카테고리를 나눴드아)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.back.domain.studyroom.entity;

import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.time.LocalDateTime;

/**
* 방별 아바타 설정 테이블
* - MEMBER 등급 이상만 저장됨 (VISITOR는 저장 안함)
* - 사용자가 아바타를 변경하면 이 테이블에 기록
* - 재입장 시 저장된 아바타 자동 로드
*/
@Entity
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "room_member_avatars",
uniqueConstraints = @UniqueConstraint(columnNames = {"room_id", "user_id"}))
public class RoomMemberAvatar extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id", nullable = false)
private Room room;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "avatar_id", nullable = false)
private Avatar selectedAvatar;

private LocalDateTime updatedAt;

/**
* 선택한 아바타 변경
*/
public void setSelectedAvatar(Avatar newAvatar) {
this.selectedAvatar = newAvatar;
this.updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.back.domain.studyroom.repository;

import com.back.domain.studyroom.entity.Avatar;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface AvatarRepository extends JpaRepository<Avatar, Long> {

/**
* 정렬 순서대로 모든 아바타 조회
*/
List<Avatar> findAllByOrderBySortOrderAsc();

/**
* 기본 아바타만 조회 (랜덤 배정용)
*/
List<Avatar> findByIsDefaultTrueOrderBySortOrderAsc();

/**
* 카테고리별 아바타 조회하도록 하는 (고양이 말고도 다른 귀여운 애들을 대비해서 추후 확장용)
*/
List<Avatar> findByCategoryOrderBySortOrderAsc(String category);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.domain.studyroom.repository;

import com.back.domain.studyroom.entity.RoomMemberAvatar;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Repository
public interface RoomMemberAvatarRepository extends JpaRepository<RoomMemberAvatar, Long> {

/**
* 특정 방에서 특정 사용자의 아바타 설정 조회
*/
Optional<RoomMemberAvatar> findByRoomIdAndUserId(Long roomId, Long userId);

/**
* 특정 방의 모든 아바타 설정 조회 (일괄 조회용)
*/
@Query("SELECT rma FROM RoomMemberAvatar rma " +
"JOIN FETCH rma.selectedAvatar " +
"WHERE rma.room.id = :roomId AND rma.user.id IN :userIds")
List<RoomMemberAvatar> findByRoomIdAndUserIdIn(@Param("roomId") Long roomId,
@Param("userIds") Set<Long> userIds);
}
Loading