Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions assets/stomp-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@
}

const socket = new SockJS(
"http://localhost:8080/ws",
"http://localhost:8080/ws-stomp",
null,
{ transports: ["xhr-streaming", "xhr-polling"] } // ✅ opCode=7 우회
);
Expand Down Expand Up @@ -442,7 +442,7 @@
}

const rid = roomIdValue();
const dest = `/topic/chat/rooms/${rid}`;
const dest = `/sub/chat/rooms/${rid}`;
$("roomTitle").textContent = `room: ${rid}`;

currentSub = stompClient.subscribe(dest, (frame) => {
Expand All @@ -459,10 +459,10 @@
});
}
});
stompClient.send("/app/chat.join", {}, JSON.stringify({
stompClient.send("/pub/chat.join", {}, JSON.stringify({
roomId: Number(roomIdValue()),
userId: myUserId(),
nickname: myNickname()
senderUserId: myUserId(),
senderNickname: myNickname()
}));
setSub("subscribed: " + dest);
debug("[SUBSCRIBED] " + dest);
Expand Down Expand Up @@ -490,7 +490,7 @@
content: text
};

stompClient.send("/app/chat.send", {}, JSON.stringify(payload));
stompClient.send("/pub/chat.send", {}, JSON.stringify(payload));
debug("[SENT] " + JSON.stringify(payload));

contentEl.value = "";
Expand All @@ -501,10 +501,10 @@

// 🔽 여기 먼저
if (stompClient && stompClient.connected) {
stompClient.send("/app/chat.leave", {}, JSON.stringify({
stompClient.send("/pub/chat.leave", {}, JSON.stringify({
roomId: Number(roomIdValue()),
userId: myUserId(),
nickname: myNickname()
senderUserId: myUserId(),
senderNickname: myNickname()
}));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.be.sportizebe.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
@NotBlank(message = "아이디를 입력해주세요.")
@Email(message = "아이디는 이메일 형식만 지원합니다.")
@Schema(description = "사용자 아이디(이메일 형식)", example = "user@example.com")
String username,

@NotBlank(message = "비밀번호를 입력해주세요.")
@Schema(description = "비밀번호", example = "password123")
String password
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ public class ChatRoomResponse {
private String name;
private boolean active;
private String chatRoomType;
private Integer maxMembers;

// 1대1 채팅방(쪽지)용 필드
private Long postId;
Expand All @@ -23,19 +22,20 @@ public class ChatRoomResponse {
public static ChatRoomResponse from(ChatRoom r) {
ChatRoomResponseBuilder builder = ChatRoomResponse.builder()
.roomId(r.getId())
.name(r.getName())
.active(r.isActive())
.chatRoomType(r.getChatRoomType() != null ? r.getChatRoomType().name() : null)
.maxMembers(r.getMaxMembers());
.chatRoomType(r.getChatRoomType() != null ? r.getChatRoomType().name() : null);

// 1대1 채팅방인 경우 추가 정보 설정
if (r.getPost() != null) {
// 타입별 name 파생
if (r.getChatRoomType() == ChatRoom.ChatRoomType.NOTE && r.getPost() != null) {
builder.name(r.getPost().getTitle());
builder.postId(r.getPost().getId());
// hostUser는 게시글 작성자로 파생
builder.hostUserId(r.getPost().getUser().getId())
.hostUsername(r.getPost().getUser().getUsername());
} else if (r.getChatRoomType() == ChatRoom.ChatRoomType.GROUP && r.getClub() != null) {
builder.name(r.getClub().getName());
}
if (r.getHostUser() != null) {
builder.hostUserId(r.getHostUser().getId())
.hostUsername(r.getHostUser().getUsername());
}

if (r.getGuestUser() != null) {
builder.guestUserId(r.getGuestUser().getId())
.guestUsername(r.getGuestUser().getUsername());
Expand Down
16 changes: 7 additions & 9 deletions src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.be.sportizebe.domain.chat.entity;


import com.be.sportizebe.domain.club.entity.Club;
import com.be.sportizebe.domain.post.entity.Post;
import com.be.sportizebe.domain.user.entity.User;
import jakarta.persistence.*;
Expand All @@ -25,12 +26,8 @@ public enum ChatRoomType {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name="name", nullable = false, length = 100)
private String name;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Integer maxMembers; // 채팅방 최대 정원

private ChatRoomType chatRoomType; // 채팅방 타입 (단체, 1:1)

@Column(name="is_active", nullable = false)
Expand All @@ -39,15 +36,16 @@ public enum ChatRoomType {
@Column(name="created_at", nullable = false, updatable = false)
private Instant createdAt;

// 동호회(GROUP) 채팅방용 필드
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "club_id")
private Club club;

// 1대1 채팅(쪽지)용 필드
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post; // 연관 게시글

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "host_user_id")
private User hostUser; // 게시글 작성자

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "guest_user_id")
private User guestUser; // 채팅 요청자
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {

// 특정 게시글에 대해 두 사용자 간의 1대1 채팅방이 이미 존재하는지 확인
Optional<ChatRoom> findByPostAndHostUserAndGuestUser(Post post, User hostUser, User guestUser);
Optional<ChatRoom> findByPostAndGuestUser(Post post, User guestUser);

// 사용자가 참여한 1대1 채팅방 목록 조회 (host 또는 guest로 참여)
List<ChatRoom> findByHostUserOrGuestUser(User hostUser, User guestUser);
// 사용자가 참여한 1대1 채팅방 목록 조회 (게시글 작성자 또는 채팅 요청자로 참여)
List<ChatRoom> findByPost_UserOrGuestUser(User postUser, User guestUser);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.be.sportizebe.domain.chat.entity.ChatRoom;
import com.be.sportizebe.domain.chat.exception.ChatErrorCode;
import com.be.sportizebe.domain.chat.repository.ChatRoomRepository;
import com.be.sportizebe.domain.club.entity.Club;
import com.be.sportizebe.domain.post.entity.Post;
import com.be.sportizebe.domain.user.entity.User;
import com.be.sportizebe.global.exception.CustomException;
Expand All @@ -19,11 +20,11 @@ public class ChatRoomService {
private final ChatRoomRepository chatRoomRepository;

@Transactional
public ChatRoom createGroup(String name) {
public ChatRoom createGroup(Club club) {
ChatRoom room = ChatRoom.builder()
.name(name)
.chatRoomType(ChatRoom.ChatRoomType.GROUP)
.build();
.chatRoomType(ChatRoom.ChatRoomType.GROUP)
.club(club)
.build();
return chatRoomRepository.save(room);
}

Expand All @@ -37,14 +38,11 @@ public ChatRoom createNote(Post post, User guestUser) {
}

// 이미 존재하는 채팅방이 있으면 반환
return chatRoomRepository.findByPostAndHostUserAndGuestUser(post, hostUser, guestUser)
return chatRoomRepository.findByPostAndGuestUser(post, guestUser)
.orElseGet(() -> {
ChatRoom room = ChatRoom.builder()
.name(post.getTitle())
.chatRoomType(ChatRoom.ChatRoomType.NOTE)
.maxMembers(2)
.post(post)
.hostUser(hostUser)
.guestUser(guestUser)
.build();
return chatRoomRepository.save(room);
Expand All @@ -53,7 +51,7 @@ public ChatRoom createNote(Post post, User guestUser) {

// 사용자가 참여한 1대1 채팅방 목록 조회
public List<ChatRoom> findMyNoteRooms(User user) {
return chatRoomRepository.findByHostUserOrGuestUser(user, user);
return chatRoomRepository.findByPost_UserOrGuestUser(user, user);
}

public List<ChatRoom> findAll() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.be.sportizebe.domain.club.controller;

import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest;
import com.be.sportizebe.domain.club.dto.request.ClubUpdateRequest;
import com.be.sportizebe.domain.club.dto.response.ClubResponse;
import com.be.sportizebe.domain.club.service.ClubServiceImpl;
import com.be.sportizebe.domain.user.entity.SportType;
import com.be.sportizebe.domain.user.entity.User;
import com.be.sportizebe.global.response.BaseResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/clubs")
@Tag(name = "club", description = "동호회 관련 API")
public class ClubController {

private final ClubServiceImpl clubService;

@PostMapping("")
@Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다.")
public ResponseEntity<BaseResponse<ClubResponse>> createClub(
@RequestBody @Valid ClubCreateRequest request,
@AuthenticationPrincipal User user) {
ClubResponse response = clubService.createClub(request, user);
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.success("동호회 생성 성공", response));
}
Comment on lines +30 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# CustomUserDetails 클래스 위치 및 구현 확인
find . -type f -name "*.java" | xargs grep -l "class CustomUserDetails"

Repository: Sportize/be

Length of output: 127


🏁 Script executed:

# CustomUserDetails 구현 세부사항 확인
rg -n "class CustomUserDetails" -A 30

Repository: Sportize/be

Length of output: 2962


🏁 Script executed:

# User 엔티티 구조 확인
find . -type f -name "*.java" -path "*/domain/*" | xargs grep -l "class User"

Repository: Sportize/be

Length of output: 265


🏁 Script executed:

cat -n src/main/java/com/be/sportizebe/global/security/CustomUserDetails.java

Repository: Sportize/be

Length of output: 1570


@AuthenticationPrincipal 주입 타입 변경 필요

CustomUserDetailsUserDetails 인터페이스를 구현하며 User를 상속하지 않습니다. @AuthenticationPrincipal의 기본 동작은 Authentication.getPrincipal()에서 CustomUserDetails 인스턴스를 반환하므로, 현재 @AuthenticationPrincipal User user로 직접 User 엔티티를 주입받을 수 없습니다.

다음 중 하나로 변경하세요:

  • @AuthenticationPrincipal(expression = "principal.user") User user 사용
  • 또는 @AuthenticationPrincipal CustomUserDetails customUserDetails로 변경하고 customUserDetails.getUser()로 User 엔티티 접근
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java`
around lines 30 - 36, The createClub method in ClubController currently injects
a User via `@AuthenticationPrincipal` User user but the principal is a
CustomUserDetails (which implements UserDetails and wraps the User entity);
change the parameter to either `@AuthenticationPrincipal`(expression =
"principal.user") User user OR to `@AuthenticationPrincipal` CustomUserDetails
customUserDetails and then call clubService.createClub(request,
customUserDetails.getUser()) so the actual User entity is passed to
ClubController.createClub.


@PutMapping("/{clubId}")
@Operation(summary = "동호회 수정", description = "동호회 정보를 수정합니다. 동호회장만 수정할 수 있습니다.")
public ResponseEntity<BaseResponse<ClubResponse>> updateClub(
@PathVariable Long clubId,
@RequestBody @Valid ClubUpdateRequest request,
@AuthenticationPrincipal User user) {
ClubResponse response = clubService.updateClub(clubId, request, user);
return ResponseEntity.ok(BaseResponse.success("동호회 수정 성공", response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.be.sportizebe.domain.club.dto.request;

import com.be.sportizebe.domain.club.entity.Club;
import com.be.sportizebe.domain.user.entity.SportType;
import com.be.sportizebe.domain.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record ClubCreateRequest(
@NotBlank(message = "동호회 이름은 필수 입니다.")
@Schema(description = "동호회 이름", example = "축구 동호회") String name,
@Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce,
@Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType,
@Schema(description = "최대 정원", example = "20") Integer maxMembers) {
// 관련 종목은 파라미터로 받음
// TODO : S3 세팅 후 imgUrl은 multipartform으로 변경

public Club toEntity(User user) {
return Club.builder()
.name(name)
.introduce(introduce)
.clubType(clubType)
.maxMembers(maxMembers)
.leader(user)
.build();
Comment on lines +9 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

maxMembers 검증이 누락되어 있습니다.

Line 13에서 maxMembers가 null/음수여도 통과할 수 있어 저장 시 제약 위반 또는 런타임 오류가 날 수 있습니다. 최소한 null/양수 검증을 추가해 주세요.

🔧 제안 수정
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;

 public record ClubCreateRequest(
   `@NotBlank`(message = "동호회 이름은 필수 입니다.")
   `@Schema`(description = "동호회 이름", example = "축구 동호회") String name,
   `@Schema`(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce,
-  `@Schema`(description = "최대 정원", example = "20") Integer maxMembers) {
+  `@NotNull`(message = "최대 정원은 필수 입니다.")
+  `@Positive`(message = "최대 정원은 양수여야 합니다.")
+  `@Schema`(description = "최대 정원", example = "20") Integer maxMembers) {
🤖 Prompt for AI Agents
In
`@src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java`
around lines 9 - 23, ClubCreateRequest currently allows maxMembers to be null or
non-positive which can cause DB/runtime errors; add validation annotations to
the record parameter by annotating maxMembers with `@NotNull` and `@Min`(1) (e.g.,
`@NotNull`(message="최대 정원은 필수 입니다.") `@Min`(value=1, message="최대 정원은 1 이상이어야 합니다.")
Integer maxMembers) and import javax.validation.constraints.NotNull and
javax.validation.constraints.Min; additionally, defensively check in toEntity
(method ClubCreateRequest.toEntity) and throw a clear IllegalArgumentException
or similar if maxMembers is null or <1 before calling Club.builder() to ensure
safe construction.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.be.sportizebe.domain.club.dto.request;

import com.be.sportizebe.domain.user.entity.SportType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record ClubUpdateRequest(
@NotBlank(message = "동호회 이름은 필수 입니다.")
@Schema(description = "동호회 이름", example = "축구 동호회") String name,
@Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce,
@Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType,
@Schema(description = "최대 정원", example = "20") Integer maxMembers) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.be.sportizebe.domain.club.dto.response;

import com.be.sportizebe.domain.club.entity.Club;
import com.be.sportizebe.domain.user.entity.SportType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
@Schema(title = "ClubResponse DTO", description = "동호회 관련 응답")
public record ClubResponse(
@Schema(description = "동호회 ID", example = "1") Long clubId,
@Schema(description = "동호회 이름", example = "축구 동호회") String name,
@Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce,
@Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType,
@Schema(description = "최대 정원", example = "20") Integer maxMembers,
@Schema(description = "동호회장 닉네임", example = "닉네임") String leaderNickname) {

public static ClubResponse from(Club club) {
return ClubResponse.builder()
.clubId(club.getId())
.name(club.getName())
.introduce(club.getIntroduce())
.clubType(club.getClubType())
.maxMembers(club.getMaxMembers())
.leaderNickname(club.getLeader().getNickname())
.build();
}
}
57 changes: 57 additions & 0 deletions src/main/java/com/be/sportizebe/domain/club/entity/Club.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.be.sportizebe.domain.club.entity;

import com.be.sportizebe.domain.chat.entity.ChatRoom;
import com.be.sportizebe.domain.user.entity.SportType;
import com.be.sportizebe.domain.user.entity.User;
import com.be.sportizebe.global.common.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "clubs")
public class Club extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String name; // 동호회 이름 == 단테 채팅방 이름

@Column(columnDefinition = "TEXT")
private String introduce; // 동호회 소개글

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private SportType clubType; // 동호회 관련 종목 (동호회 생성 시 선택)

@Column(nullable = false)
private Integer maxMembers; // 최대 정원

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "leader_id", nullable = false)
private User leader; // 동호회장

@OneToOne(mappedBy = "club", fetch = FetchType.LAZY)
private ChatRoom chatRoom; // 동호회 채팅방

@OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<ClubMember> members = new ArrayList<>();

public void update(String name, String introduce, Integer maxMembers, SportType clubType) {
this.name = name;
this.introduce = introduce;
this.maxMembers = maxMembers;
this.clubType = clubType;
}
}
Loading