Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import konkuk.thip.common.exception.AuthException;
Expand Down Expand Up @@ -68,10 +69,20 @@ protected void doFilterInternal(HttpServletRequest request,
}

private String extractToken(HttpServletRequest request) {
// 1. Authorization 헤더 우선 (앱)
String authorization = request.getHeader(JWT_HEADER_KEY.getValue());
if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) {
return authorization.split(" ")[1];
}
Comment on lines 74 to 76
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Authorization 헤더 파싱 취약 — split 인덱싱으로 AIOOB 가능

"Bearer" 다음에 공백/토큰이 없거나 공백이 여러 개인 경우 ArrayIndexOutOfBoundsException 가능성이 있습니다. prefix 길이 기반으로 안전하게 파싱하세요.

적용 제안:

-            return authorization.split(" ")[1];
+            return authorization.substring(JWT_PREFIX.getValue().length()).trim();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) {
return authorization.split(" ")[1];
}
if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) {
return authorization.substring(JWT_PREFIX.getValue().length()).trim();
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
around lines 74-76, the current authorization header parsing uses
authorization.split(" ")[1] which can throw ArrayIndexOutOfBoundsException when
the header has no token or extra spaces; instead compute the token by taking the
substring after the known prefix length (e.g.
authorization.substring(JWT_PREFIX.getValue().length())), trim the result, and
validate it is non-empty before returning (otherwise return null or handle as
unauthenticated); ensure you keep the startsWith check and avoid indexing into
split arrays.


// 2. Cookie에서 JWT 추출 (웹)
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
return cookie.getValue();
}
}
}
Comment on lines +78 to +85
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

쿠키에서 JWT 조회 시 키 상수 혼용 가능성 — 전용 쿠키 키 사용 검토 및 두 케이스 지원

현재 쿠키 이름 비교에 JWT_HEADER_KEY(통상 Authorization 헤더 키)를 재사용하고 있습니다. 일반적으로 쿠키 키는 헤더 키와 분리(JWT_COOKIE_KEY 등)하는 것이 명확합니다. 또한 쿠키 값에 "Bearer " 프리픽스가 포함될 가능성까지 대비해 제거 로직을 넣어두는 편이 안전합니다.

최소 보완(프리픽스 제거) 제안:

-                if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
-                    return cookie.getValue();
-                }
+                if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
+                    String value = cookie.getValue();
+                    return value.startsWith(JWT_PREFIX.getValue())
+                            ? value.substring(JWT_PREFIX.getValue().length()).trim()
+                            : value;
+                }

추가 권장:

  • 전용 쿠키 키 상수(JWT_COOKIE_KEY 등) 도입 후 해당 키 우선 조회, 레거시 호환을 위해 한시적으로 HEADER_KEY 이름도 병행 지원.
  • SameSite, Secure, HttpOnly 설정은 토큰 발급/설정 시점에서 반드시 보장.

레포 내 전용 쿠키 키 상수 존재 여부와 사용 위치 확인 스크립트:


🏁 Script executed:

#!/bin/bash
# JWT 쿠키 관련 상수 및 사용처 탐색
rg -n "JWT_.*COOKIE" -S
rg -n "AuthParameters" -A 5 -B 3
# Authorization 헤더/쿠키 사용 패턴 확인
rg -n "JWT_HEADER_KEY"
# 쿠키 생성/추가 위치 탐색
rg -n 'new Cookie\(' -A 2
rg -n 'addCookie\(' -A 2

Length of output: 10136


전용 JWT 쿠키 키 분리, Bearer 프리픽스 제거 및 보안 속성 추가 필요

현재 Authorization 헤더 키(JWT_HEADER_KEY)를 쿠키 이름으로도 재사용 중이며, 쿠키 값에 대한 Bearer 프리픽스 제거 로직과 HttpOnly/Secure/SameSite 설정이 빠져 있습니다. 아래 항목을 반영해 주세요.

AuthParameters.java
JWT_COOKIE_KEY 상수 추가
JwtAuthenticationFilter.java
– 쿠키 조회 시 JWT_COOKIE_KEY 우선 조회, 레거시 호환을 위해 JWT_HEADER_KEY도 지원
– 쿠키 값에 JWT_PREFIX(“Bearer ”) 제거 로직 추가
CustomSuccessHandler.java
– 쿠키 생성 시 JWT_COOKIE_KEY 사용
HttpOnly, Secure, Path 설정 보장
– SameSite 설정 추가 (필요 시 Set-Cookie 헤더 직접 조작)

예시 변경안:

src/main/java/konkuk/thip/common/security/constant/AuthParameters.java

 public enum AuthParameters {
     JWT_HEADER_KEY("Authorization"),
     JWT_PREFIX("Bearer "),
+    JWT_COOKIE_KEY("access_token"), // 원하는 쿠키 이름으로 변경
     KAKAO("kakao"),
     GOOGLE("google"),
     KAKAO_PROVIDER_ID_KEY("id");
     // …
 }

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java

- if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
-     String value = cookie.getValue();
-     return value.startsWith(JWT_PREFIX.getValue())
-         ? value.substring(JWT_PREFIX.getValue().length()).trim()
-         : value;
- }
+ if (JWT_COOKIE_KEY.getValue().equals(cookie.getName())
+     || JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
+     String value = cookie.getValue();
+     return value.startsWith(JWT_PREFIX.getValue())
+         ? value.substring(JWT_PREFIX.getValue().length()).trim()
+         : value;
+ }

src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java

- Cookie cookie = new Cookie(JWT_HEADER_KEY.getValue(), token);
+ Cookie cookie = new Cookie(JWT_COOKIE_KEY.getValue(), token);
+ cookie.setHttpOnly(true);
+ cookie.setSecure(true);
+ cookie.setPath("/");
+ // SameSite 설정이 필요할 경우 아래와 같이 직접 헤더를 조작
+ // response.setHeader("Set-Cookie",
+ //     String.format("%s=%s; Path=/; Secure; HttpOnly; SameSite=Strict", cookie.getName(), cookie.getValue()));
 response.addCookie(cookie);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 2. Cookie에서 JWT 추출 (웹)
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
return cookie.getValue();
}
}
}
// 2. Cookie에서 JWT 추출 (웹)
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
- if (JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
- return cookie.getValue();
- }
+ if (JWT_COOKIE_KEY.getValue().equals(cookie.getName())
+ || JWT_HEADER_KEY.getValue().equals(cookie.getName())) {
+ String value = cookie.getValue();
+ return value.startsWith(JWT_PREFIX.getValue())
+ ? value.substring(JWT_PREFIX.getValue().length()).trim()
+ : value;
+ }
}
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
around lines 78-85, the code reuses JWT_HEADER_KEY as the cookie name and
doesn't strip a Bearer prefix or prefer a dedicated cookie key; add a new
constant JWT_COOKIE_KEY in AuthParameters, update the filter to first look for a
cookie named JWT_COOKIE_KEY (fall back to JWT_HEADER_KEY for legacy), strip the
JWT_PREFIX ("Bearer ") from the cookie value if present, and return the cleaned
token; also update CustomSuccessHandler to set the cookie using JWT_COOKIE_KEY
and ensure HttpOnly, Secure, Path are set and SameSite is applied (use direct
Set-Cookie header manipulation if your cookie API lacks SameSite support).

log.info("토큰이 없습니다.");
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public record MemberSearchResult(
String nickname,
String imageUrl,
String aliasName,
String aliasColor,
int followerCount
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public record RoomPlayingDetailViewResponse(
String progressStartDate,
String progressEndDate,
String category,
String categoryColor,
String roomDescription,
int memberCount,
int recruitCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.List;

@Builder
public record RoomRecruitingDetailViewResponse(
boolean isHost,
boolean isJoining,
Expand All @@ -15,6 +16,7 @@ public record RoomRecruitingDetailViewResponse(
String progressEndDate,
String recruitEndDate,
String category,
String categoryColor,
String roomDescription,
int memberCount,
int recruitCount,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package konkuk.thip.room.adapter.out.persistence;

import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.common.exception.code.ErrorCode;
import konkuk.thip.common.util.Cursor;
import konkuk.thip.common.util.CursorBasedList;
import konkuk.thip.room.adapter.in.web.response.RoomGetHomeJoinedListResponse;
import konkuk.thip.room.adapter.in.web.response.RoomRecruitingDetailViewResponse;
import konkuk.thip.room.adapter.in.web.response.RoomSearchResponse;
import konkuk.thip.room.adapter.out.persistence.function.RoomQueryFunction;
import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository;
import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository;
import konkuk.thip.room.application.port.out.RoomQueryPort;
import konkuk.thip.room.application.port.out.dto.RoomQueryDto;
import konkuk.thip.room.domain.Category;
Expand All @@ -24,6 +27,7 @@
public class RoomQueryPersistenceAdapter implements RoomQueryPort {

private final RoomJpaRepository roomJpaRepository;
private final CategoryJpaRepository categoryJpaRepository;

@Override
public int countRecruitingRoomsByBookAndStartDateAfter(String isbn, LocalDate currentDate) {
Expand Down Expand Up @@ -101,4 +105,12 @@ public List<RoomQueryDto> findRoomsByCategoryOrderByDeadline(Category category,
public List<RoomQueryDto> findRoomsByCategoryOrderByPopular(Category category, int limit, Long userId) {
return roomJpaRepository.findRoomsByCategoryOrderByMemberCount(category.getValue(), limit, userId);
}

// TODO : 리펙토링 대상
@Override
public String findAliasColorOfCategory(Category category) {
return categoryJpaRepository.findAliasColorByValue(category.getValue()).orElseThrow(
() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface CategoryJpaRepository extends JpaRepository<CategoryJpaEntity, Long> {

Optional<CategoryJpaEntity> findByValue(String value);

// TODO : 리펙토링 대상
@Query("select a.color " +
"from CategoryJpaEntity c join c.aliasForCategoryJpaEntity a " +
"where c.value = :categoryValue")
Optional<String> findAliasColorByValue(@Param("categoryValue") String categoryValue);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@ public interface RoomQueryPort {

List<RoomQueryDto> findRoomsByCategoryOrderByPopular(Category category, int limit, Long userId);


/**
* 임시 메서드
* TODO 리펙토링 대상
*/
String findAliasColorOfCategory(Category category);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public RoomGetMemberListResponse getRoomMemberList(Long roomId) {
.nickname(user.getNickname())
.imageUrl(user.getAlias().getImageUrl())
.aliasName(user.getAlias().getValue())
.aliasColor(user.getAlias().getColor())
.followerCount(user.getFollowerCount())
.build();
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import konkuk.thip.room.adapter.in.web.response.RoomPlayingDetailViewResponse;
import konkuk.thip.room.application.port.in.RoomShowPlayingDetailViewUseCase;
import konkuk.thip.room.application.port.out.RoomCommandPort;
import konkuk.thip.room.application.port.out.RoomQueryPort;
import konkuk.thip.room.domain.Room;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.domain.RoomParticipants;
Expand All @@ -24,14 +25,15 @@ public class RoomShowPlayingDetailViewService implements RoomShowPlayingDetailVi
private static final int TOP_PARTICIPATION_VOTES_COUNT = 3;

private final RoomCommandPort roomCommandPort;
private final RoomQueryPort roomQueryPort;
private final BookCommandPort bookCommandPort;
private final RoomParticipantCommandPort roomParticipantCommandPort;
private final VoteQueryPort voteQueryPort;

@Override
@Transactional(readOnly = true)
public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId) {
// 1. Room 조회, Book 조회
// 1. Room 조회, Book 조회, Category와 연관된 Alias 조회
Room room = roomCommandPort.getByIdOrThrow(roomId);
Book book = bookCommandPort.findById(room.getBookId());

Expand Down Expand Up @@ -66,6 +68,7 @@ private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book
.currentPage(roomParticipants.getCurrentPageOfUser(userId))
.userPercentage(roomParticipants.getUserPercentageOfUser(userId))
.currentVotes(topParticipationVotes)
.categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

카테고리 색상 조회 위치 재검토 필요

현재 buildResponse 메서드 내에서 roomQueryPort.findAliasColorOfCategory()를 호출하고 있습니다. 이는 순수한 응답 빌딩 로직에 외부 포트 호출이 섞여 있어 단일 책임 원칙에 위배됩니다.

다음과 같이 서비스 메서드에서 미리 조회하는 것을 권장합니다:

 @Override
 @Transactional(readOnly = true)
 public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId) {
     // 1. Room 조회, Book 조회, Category와 연관된 Alias 조회
     Room room = roomCommandPort.getByIdOrThrow(roomId);
     Book book = bookCommandPort.findById(room.getBookId());
+    String categoryColor = roomQueryPort.findAliasColorOfCategory(room.getCategory());
 
     // 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성
     // TODO. Room 도메인에 memberCount 값 추가된 후 리펙토링
     List<RoomParticipant> findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId);
     RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId);
 
     // 3. 투표 참여율이 가장 높은 투표 조회
     List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes = voteQueryPort.findTopParticipationVotesByRoom(room, TOP_PARTICIPATION_VOTES_COUNT);
 
     // 4. response 구성
-    return buildResponse(userId, room, book, roomParticipants, topParticipationVotes);
+    return buildResponse(userId, room, book, roomParticipants, topParticipationVotes, categoryColor);
 }

-private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book book, RoomParticipants roomParticipants, List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes) {
+private RoomPlayingDetailViewResponse buildResponse(Long userId, Room room, Book book, RoomParticipants roomParticipants, List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes, String categoryColor) {
     return RoomPlayingDetailViewResponse.builder()
             // ... 기존 필드들 ...
-            .categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory()))      // TODO : 리펙토링 대상
+            .categoryColor(categoryColor)
             .build();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상
@Override
@Transactional(readOnly = true)
public RoomPlayingDetailViewResponse getPlayingRoomDetailView(Long userId, Long roomId) {
// 1. Room 조회, Book 조회, Category와 연관된 Alias 조회
Room room = roomCommandPort.getByIdOrThrow(roomId);
Book book = bookCommandPort.findById(room.getBookId());
+ String categoryColor = roomQueryPort.findAliasColorOfCategory(room.getCategory());
// 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성
List<RoomParticipant> findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId);
RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId);
// 3. 투표 참여율이 가장 높은 투표 조회
List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes =
voteQueryPort.findTopParticipationVotesByRoom(room, TOP_PARTICIPATION_VOTES_COUNT);
// 4. response 구성
- return buildResponse(userId, room, book, roomParticipants, topParticipationVotes);
+ return buildResponse(userId, room, book, roomParticipants, topParticipationVotes, categoryColor);
}
-private RoomPlayingDetailViewResponse buildResponse(
- Long userId,
- Room room,
- Book book,
- RoomParticipants roomParticipants,
- List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes
-) {
+private RoomPlayingDetailViewResponse buildResponse(
+ Long userId,
+ Room room,
+ Book book,
+ RoomParticipants roomParticipants,
+ List<RoomPlayingDetailViewResponse.CurrentVote> topParticipationVotes,
+ String categoryColor
+) {
return RoomPlayingDetailViewResponse.builder()
// ... 기존 필드들 ...
- .categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상
+ .categoryColor(categoryColor)
.build();
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/application/service/RoomShowPlayingDetailViewService.java
around line 71, the buildResponse method currently calls
roomQueryPort.findAliasColorOfCategory(...) which mixes an external port call
into response-building; move that lookup into the service method before calling
buildResponse: call roomQueryPort.findAliasColorOfCategory(room.getCategory())
once in the service, pass the resulting categoryColor as an additional argument
into buildResponse (or into the DTO constructor), and remove the port call from
buildResponse so the builder only composes data it is given; update any method
signatures and call sites accordingly.

.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,28 @@ private RoomRecruitingDetailViewResponse buildResponse(
RoomParticipants participants,
List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms
) {
return new RoomRecruitingDetailViewResponse(
participants.isHostOfRoom(userId),
participants.isJoiningToRoom(userId),
room.getId(),
room.getTitle(),
room.getCategory().getImageUrl(),
room.isPublic(),
DateUtil.formatDate(room.getStartDate()),
DateUtil.formatDate(room.getEndDate()),
DateUtil.formatAfterTime(room.getStartDate()),
room.getCategory().getValue(),
room.getDescription(),
participants.calculateMemberCount(),
room.getRecruitCount(),
book.getIsbn(),
book.getImageUrl(),
book.getTitle(),
book.getAuthorName(),
book.getDescription(),
book.getPublisher(),
recommendRooms
);
return RoomRecruitingDetailViewResponse.builder()
.isHost(participants.isHostOfRoom(userId))
.isJoining(participants.isJoiningToRoom(userId))
.roomId(room.getId())
.roomName(room.getTitle())
.roomImageUrl(room.getCategory().getImageUrl())
.isPublic(room.isPublic())
.progressStartDate(DateUtil.formatDate(room.getStartDate()))
.progressEndDate(DateUtil.formatDate(room.getEndDate()))
.recruitEndDate(DateUtil.formatAfterTime(room.getStartDate()))
.category(room.getCategory().getValue())
.categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

카테고리 색상 조회 위치 개선 필요

RoomShowPlayingDetailViewService와 동일한 문제가 있습니다. buildResponse 메서드는 순수한 응답 빌딩 로직만 담당해야 합니다.

서비스 메서드에서 미리 조회하도록 수정하세요:

 @Override
 @Transactional(readOnly = true)
 public RoomRecruitingDetailViewResponse getRecruitingRoomDetailView(Long userId, Long roomId) {
     // 1. Room 조회, Book 조회
     Room room = roomCommandPort.getByIdOrThrow(roomId);
     Book book = bookCommandPort.findById(room.getBookId());
+    String categoryColor = roomQueryPort.findAliasColorOfCategory(room.getCategory());
 
     // 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성
     List<RoomParticipant> findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId);
     RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId);
 
     // 3. 다른 모임방 추천
     List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms = roomQueryPort.findOtherRecruitingRoomsByCategoryOrderByStartDateAsc(room, RECOMMEND_ROOM_COUNT);
 
     // 4. response 구성
-    return buildResponse(userId, room, book, roomParticipants, recommendRooms);
+    return buildResponse(userId, room, book, roomParticipants, recommendRooms, categoryColor);
 }

 private RoomRecruitingDetailViewResponse buildResponse(
         Long userId,
         Room room,
         Book book,
         RoomParticipants participants,
-        List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms
+        List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms,
+        String categoryColor
 ) {
     return RoomRecruitingDetailViewResponse.builder()
             // ... 기존 필드들 ...
-            .categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory()))      // TODO : 리펙토링 대상
+            .categoryColor(categoryColor)
             // ... 나머지 필드들 ...
             .build();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.categoryColor(roomQueryPort.findAliasColorOfCategory(room.getCategory())) // TODO : 리펙토링 대상
@Override
@Transactional(readOnly = true)
public RoomRecruitingDetailViewResponse getRecruitingRoomDetailView(Long userId, Long roomId) {
// 1. Room 조회, Book 조회
Room room = roomCommandPort.getByIdOrThrow(roomId);
Book book = bookCommandPort.findById(room.getBookId());
String categoryColor = roomQueryPort.findAliasColorOfCategory(room.getCategory());
// 2. Room과 연관된 UserRoom 조회, RoomParticipants 일급 컬렉션 생성
List<RoomParticipant> findByRoomId = roomParticipantCommandPort.findAllByRoomId(roomId);
RoomParticipants roomParticipants = RoomParticipants.from(findByRoomId);
// 3. 다른 모임방 추천
List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms =
roomQueryPort.findOtherRecruitingRoomsByCategoryOrderByStartDateAsc(room, RECOMMEND_ROOM_COUNT);
// 4. response 구성
return buildResponse(userId, room, book, roomParticipants, recommendRooms, categoryColor);
}
private RoomRecruitingDetailViewResponse buildResponse(
Long userId,
Room room,
Book book,
RoomParticipants participants,
List<RoomRecruitingDetailViewResponse.RecommendRoom> recommendRooms,
String categoryColor
) {
return RoomRecruitingDetailViewResponse.builder()
// ... 기존 필드들 ...
.categoryColor(categoryColor)
// ... 나머지 필드들 ...
.build();
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/application/service/RoomShowRecruitingDetailViewService.java
around line 67, the buildResponse currently calls
roomQueryPort.findAliasColorOfCategory(...) which mixes data fetching into
presentation building; move the category color lookup into the service method
before calling buildResponse, store the result in a local variable, and pass
that color into buildResponse (or into the DTO/builder) so buildResponse remains
pure presentation logic; also apply the same change pattern to
RoomShowPlayingDetailViewService to keep consistency.

.roomDescription(room.getDescription())
.memberCount(participants.calculateMemberCount())
.recruitCount(room.getRecruitCount())
.isbn(book.getIsbn())
.bookImageUrl(book.getImageUrl())
.bookTitle(book.getTitle())
.authorName(book.getAuthorName())
.bookDescription(book.getDescription())
.publisher(book.getPublisher())
.recommendRooms(recommendRooms)
.build();
}
}