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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@RequiredArgsConstructor(access = AccessLevel.PUBLIC)
@Getter(AccessLevel.PUBLIC)
public enum AppErrorCode implements ErrorCode {
DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT, "공통: 요청한 데이터가 유효하지 않아 저장할 수 없습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "공통: 서버 내부 오류가 발생했습니다."),
SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "공통: 서비스가 일시적으로 불가능합니다."),
DEPENDENCY_FAILURE(HttpStatus.BAD_GATEWAY, "공통: 외부/하위 시스템 연동에 실패했습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -282,20 +283,40 @@ public ResponseEntity<ErrorResponse> handleAny(Exception ex, HttpServletRequest
}


@ExceptionHandler({
RedisConnectionFailureException.class,
RedisSystemException.class,
DataAccessException.class
})
@ExceptionHandler({RedisConnectionFailureException.class, RedisSystemException.class})
public ResponseEntity<ErrorResponse> handleRedis(Exception ex, HttpServletRequest request) {
log.error("Redis 장애(500): {}", rootCauseMessage(ex), ex);
return handleApp(new AppException(GroupErrorCode.REDIS_READ_FAILED), request);
}

AppException mapped = new AppException(GroupErrorCode.REDIS_READ_FAILED);
return handleApp(mapped, request);
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ErrorResponse> handleDataIntegrity(
DataIntegrityViolationException ex, HttpServletRequest request) {

String msg = rootCauseMessage(ex);
log.error("DB 무결성 위반(409): {}", msg, ex);

// 예: H2 메시지에 constraint 이름이 들어옴
// "PUBLIC.UK_GROUP_ID_SORT_ORDER_INDEX_D"
if (msg != null && msg.contains("UK_GROUP_ID_SORT_ORDER_INDEX_D")) {
return handleApp(new AppException(GroupErrorCode.GROUP_IMAGE_SORT_ORDER_CONFLICT), request);
}

// 나머지는 공통 무결성 위반 코드로 (AppErrorCode 하나 만드는 걸 추천)
return handleApp(new AppException(AppErrorCode.DATA_INTEGRITY_VIOLATION), request);
}

@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ErrorResponse> handleDataAccess(
DataAccessException ex, HttpServletRequest request) {

log.error("DB 접근 오류(500): {}", rootCauseMessage(ex), ex);
return handleApp(new AppException(AppErrorCode.INTERNAL_SERVER_ERROR), request);
}

@ExceptionHandler(JsonProcessingException.class)
public ResponseEntity<ErrorResponse> handleJson(JsonProcessingException ex, HttpServletRequest request) {
public ResponseEntity<ErrorResponse> handleJson(JsonProcessingException ex,
HttpServletRequest request) {
log.error("Jackson 직렬화/역직렬화 실패(500): {}", rootCauseMessage(ex), ex);
AppException mapped = new AppException(GroupErrorCode.REDIS_READ_FAILED);
return handleApp(mapped, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,34 @@
@Getter
@RequiredArgsConstructor
public enum GroupErrorCode implements ErrorCode {
GROUP_IMAGE_SORT_ORDER_CONFLICT(
HttpStatus.CONFLICT,
"모임: 이미지 정렬 처리 중 충돌이 발생했습니다. 다시 시도해주세요."
),
GROUP_IMAGE_NOT_FOUND_IN_GROUP_AFTER_UPDATE(
HttpStatus.BAD_REQUEST,
"모임: 요청한 이미지 키(%s)를 반영할 수 없습니다. (모임에 없거나 선업로드 이미지가 아닙니다.)"
),
TAG_EXCEED_MAX(HttpStatus.BAD_REQUEST, "모임: 태그는 최대 10개까지 가능합니다. (요청=%s)"),
TAG_DUPLICATED(HttpStatus.BAD_REQUEST, "모임: 태그가 중복되었습니다."),
DUPLICATED_IMAGE_KEY_IN_REQUEST(HttpStatus.BAD_REQUEST, "모임: 이미지 키가 중복되었습니다."),
IMAGE_ORDER_EMPTY_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "모임: 이미지는 최소 1장 이상이어야 합니다."),
TAG_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "모임: 태그 이름이 중복되었습니다."),

GROUP_TITLE_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 제목은 필수입니다."),
GROUP_TITLE_TOO_LONG(HttpStatus.BAD_REQUEST, "모임: 제목은 50자 이하여야 합니다."),
GROUP_DESCRIPTION_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 설명은 필수입니다."),
GROUP_DESCRIPTION_TOO_LONG(HttpStatus.BAD_REQUEST, "모임: 설명은 300자 이하여야 합니다."),

GROUP_TIME_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 시작 시간은 필수입니다."),
GROUP_TIME_INVALID_RANGE(HttpStatus.BAD_REQUEST, "모임: 시작/종료 시간이 올바르지 않습니다. (start < end)"),
MAX_PARTICIPANTS_BELOW_ATTEND_COUNT(HttpStatus.CONFLICT, "모임: 현재 참석자 수(%s)보다 정원을 줄일 수 없습니다."),
GROUP_ONLY_HOST_CAN_UPDATE(HttpStatus.FORBIDDEN, "모임: 수정 권한이 없습니다."),
GROUP_CANNOT_UPDATE_IN_STATUS(HttpStatus.CONFLICT, "모임: 현재 상태(%s)에서는 수정할 수 없습니다."),
GROUP_DELETED(HttpStatus.NOT_FOUND, "모임: 삭제된 모임입니다."),
INVALID_COOLDOWN_SECONDS(HttpStatus.BAD_REQUEST, "모임: 유효하지 않은 쿨다운 정책입니다."),
GROUP_CREATE_COOLDOWN_ACTIVE(HttpStatus.TOO_MANY_REQUESTS, "모임: 모임 생성은 연속으로 할 수 없습니다. {%s}초 후 다시 시도해 주세요."),
GROUP_CREATE_COOLDOWN_ACTIVE(HttpStatus.TOO_MANY_REQUESTS,
"모임: 모임 생성은 연속으로 할 수 없습니다. {%s}초 후 다시 시도해 주세요."),
PRE_UPLOADED_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "모임: 선 업로드 이미지가 만료되었거나 존재하지 않습니다."),
PRE_UPLOADED_IMAGE_OWNER_MISMATCH(HttpStatus.FORBIDDEN, "모임: 선 업로드 이미지를 업로드한 사용자만 사용할 수 있습니다."),
REDIS_SAVE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "모임: 선 업로드 이미지 저장에 실패했습니다."),
Expand Down Expand Up @@ -45,7 +70,8 @@ public enum GroupErrorCode implements ErrorCode {
GROUP_IMAGE_NOT_FOUND(HttpStatus.NOT_FOUND, "모임 이미지가 존재하지 않습니다. groupId=%d"),
LOCATION_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 모임 위치는 필수입니다."),
GROUP_STATUS_REQUIRED(HttpStatus.BAD_REQUEST, "모임: 모임 상태는 필수입니다."),
GROUP_STATUS_TRANSFER_IMPOSSIBLE(HttpStatus.BAD_REQUEST, "모임: 상태 전이가 불가능합니다. 현재 상태: %s, 요청한 상태: %s"),
GROUP_STATUS_TRANSFER_IMPOSSIBLE(HttpStatus.BAD_REQUEST,
"모임: 상태 전이가 불가능합니다. 현재 상태: %s, 요청한 상태: %s"),
USER_ID_NULL(HttpStatus.NOT_FOUND, "모임: 회원 ID가 null 입니다."),
GROUP_HOST_CANNOT_ATTEND(HttpStatus.BAD_REQUEST, "모임: HOST는 다시 모임에 신청을 할 수 없습니다."),
GROUP_NOT_RECRUITING(HttpStatus.BAD_REQUEST, "모임: 모집 상태가 아닙니다. 현재 상태: %s"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,54 @@

import java.util.List;
import team.wego.wegobackend.group.v2.domain.entity.GroupImageV2;
import team.wego.wegobackend.group.v2.domain.entity.GroupImageV2VariantType;
import team.wego.wegobackend.group.v2.domain.entity.ImageV2Format;

public record GroupImageItem(
Long groupImageId,
String imageKey,
int sortOrder,
List<GroupImageVariantItem> variants
) {

private static final String DEFAULT_100 =
"https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/default/group_logo_100x100.webp";
private static final String DEFAULT_440 =
"https://we-go-bucket.s3.ap-northeast-2.amazonaws.com/default/group_logo_440x240.webp";

public static GroupImageItem from(GroupImageV2 image) {
return new GroupImageItem(
image.getId(),
image.getImageKey(),
image.getSortOrder(),
image.getVariants().stream().map(GroupImageVariantItem::from).toList()
);
}

// DB에 이미지가 0개일 때 내려주는 기본 이미지(440/100)
public static GroupImageItem defaultLogo() {
return new GroupImageItem(
null, // DB row가 아니다. 그래서 null
"DEFAULT", // 테스트 편하게 DEFAULT로 설정
0, // 없으니까 어차피 대표
Comment on lines +29 to +34
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Comments contain Korean language. Lines 32-34 have Korean comments that should be translated to English for consistency with standard code documentation practices.

Suggested change
// DB에 이미지가 0개일 때 내려주는 기본 이미지(440/100)
public static GroupImageItem defaultLogo() {
return new GroupImageItem(
null, // DB row가 아니다. 그래서 null
"DEFAULT", // 테스트 편하게 DEFAULT로 설정
0, // 없으니까 어차피 대표
// Default images (440/100) returned when there are zero images in the DB
public static GroupImageItem defaultLogo() {
return new GroupImageItem(
null, // Not a DB row, so set to null
"DEFAULT", // Use DEFAULT for easier testing
0, // 0 indicates the primary image since there are no others

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +34
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Comment contains Korean language. The comment "DB에 이미지가 0개일 때 내려주는 기본 이미지(440/100)" should be translated to English for consistency with standard code documentation practices.

Suggested change
// DB에 이미지가 0개일 때 내려주는 기본 이미지(440/100)
public static GroupImageItem defaultLogo() {
return new GroupImageItem(
null, // DB row가 아니다. 그래서 null
"DEFAULT", // 테스트 편하게 DEFAULT로 설정
0, // 없으니까 어차피 대표
// Default image (440/100) returned when there are 0 images in the DB
public static GroupImageItem defaultLogo() {
return new GroupImageItem(
null, // Not a DB row, therefore null
"DEFAULT", // Set to DEFAULT for easier testing
0, // 0 because it is the representative image when none exist

Copilot uses AI. Check for mistakes.
List.of(
new GroupImageVariantItem(
null,
GroupImageV2VariantType.CARD_440_240,
GroupImageV2VariantType.CARD_440_240.getWidth(),
GroupImageV2VariantType.CARD_440_240.getHeight(),
ImageV2Format.WEBP,
DEFAULT_440
),
new GroupImageVariantItem(
null,
GroupImageV2VariantType.THUMBNAIL_100_100,
GroupImageV2VariantType.THUMBNAIL_100_100.getWidth(),
GroupImageV2VariantType.THUMBNAIL_100_100.getHeight(),
Comment on lines +39 to +48
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Inconsistent method calls. The code uses getWidth() and getHeight() on lines 39-40 and 47-48, but in the same class on lines 138-148 of GroupV2UpdateService, it uses width() and height() without "get" prefix. Verify that GroupImageV2VariantType enum provides both getWidth()/getHeight() methods, or update these calls to use the correct method name (likely width() and height() based on common enum patterns).

Suggested change
GroupImageV2VariantType.CARD_440_240.getWidth(),
GroupImageV2VariantType.CARD_440_240.getHeight(),
ImageV2Format.WEBP,
DEFAULT_440
),
new GroupImageVariantItem(
null,
GroupImageV2VariantType.THUMBNAIL_100_100,
GroupImageV2VariantType.THUMBNAIL_100_100.getWidth(),
GroupImageV2VariantType.THUMBNAIL_100_100.getHeight(),
GroupImageV2VariantType.CARD_440_240.width(),
GroupImageV2VariantType.CARD_440_240.height(),
ImageV2Format.WEBP,
DEFAULT_440
),
new GroupImageVariantItem(
null,
GroupImageV2VariantType.THUMBNAIL_100_100,
GroupImageV2VariantType.THUMBNAIL_100_100.width(),
GroupImageV2VariantType.THUMBNAIL_100_100.height(),

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +48
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Inconsistent method calls. The code uses getWidth() and getHeight() on lines 47-48, but in the same class on lines 146-148 of GroupV2UpdateService, it uses width() and height() without "get" prefix. Verify that GroupImageV2VariantType enum provides both getWidth()/getHeight() methods, or update these calls to use the correct method name (likely width() and height() based on common enum patterns).

Suggested change
GroupImageV2VariantType.CARD_440_240.getWidth(),
GroupImageV2VariantType.CARD_440_240.getHeight(),
ImageV2Format.WEBP,
DEFAULT_440
),
new GroupImageVariantItem(
null,
GroupImageV2VariantType.THUMBNAIL_100_100,
GroupImageV2VariantType.THUMBNAIL_100_100.getWidth(),
GroupImageV2VariantType.THUMBNAIL_100_100.getHeight(),
GroupImageV2VariantType.CARD_440_240.width(),
GroupImageV2VariantType.CARD_440_240.height(),
ImageV2Format.WEBP,
DEFAULT_440
),
new GroupImageVariantItem(
null,
GroupImageV2VariantType.THUMBNAIL_100_100,
GroupImageV2VariantType.THUMBNAIL_100_100.width(),
GroupImageV2VariantType.THUMBNAIL_100_100.height(),

Copilot uses AI. Check for mistakes.
ImageV2Format.WEBP,
DEFAULT_100
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package team.wego.wegobackend.group.v2.application.dto.request;

import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status;

/**
*
* images는 “최종 순서 리스트”로 받자! (0번이 대표) 기존 imageKey + 새 preUploaded imageKey를 섞어서 보내도 OK! 생략(null)이면
* “이미지 변경 없음” 으로 가자 빈 리스트([])면 “이미지 전체 삭제”(정책 허용 시) 하자
Comment on lines +9 to +11
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

Javadoc comment contains Korean language. The documentation should be translated to English for consistency with standard code documentation practices and better international maintainability.

Suggested change
*
* images는최종 순서 리스트 받자! (0번이 대표) 기존 imageKey + preUploaded imageKey를 섞어서 보내도 OK! 생략(null)이면
* “이미지 변경 없음으로 가자 리스트([])이미지 전체 삭제”(정책 허용 ) 하자
* images should be provided as the final ordered list (index 0 is the representative image).
* It is allowed to mix existing imageKeys with newly pre-uploaded imageKeys in this list.
* If images is omitted (null), it is treated as "no image changes". If an empty list ([]) is provided,
* it is treated as "delete all images" (when such a policy is allowed).

Copilot uses AI. Check for mistakes.
*/
public record UpdateGroupV2Request(
@Size(max = 50)
String title,
@Size(max = 300)
String description,

String location,
String locationDetail,

LocalDateTime startTime,
LocalDateTime endTime,

Integer maxParticipants,

GroupV2Status status,

@Size(max = 10)
List<String> tags,

@Size(max = 3)
List<String> imageKeys
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import java.time.LocalDateTime;
import java.util.List;
import team.wego.wegobackend.group.v2.application.dto.common.Address;
import team.wego.wegobackend.group.v2.application.dto.common.CreatedBy;
import team.wego.wegobackend.group.v2.application.dto.common.GroupImageItem;
import team.wego.wegobackend.group.v2.domain.entity.GroupTagV2;
import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2;
import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role;
import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2Address;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status;
import team.wego.wegobackend.tag.domain.entity.Tag;
import team.wego.wegobackend.user.domain.User;
Expand Down Expand Up @@ -67,16 +67,6 @@ public static CreateGroupV2Response from(GroupV2 group, User currentUser) {
);
}

public record Address(
String location,
String locationDetail
) {

public static Address from(GroupV2Address address) {
return new Address(address.getLocation(), address.getLocationDetail());
}
}

public record Membership(
Long groupUserId,
Long userId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package team.wego.wegobackend.group.v2.application.dto.response;


import java.time.LocalDateTime;
import java.util.List;
import team.wego.wegobackend.group.v2.application.dto.common.Address;
import team.wego.wegobackend.group.v2.application.dto.common.GroupImageItem;
import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status;

public record UpdateGroupV2Response(
Long id,
String title,
GroupV2Status status,
Address address,
LocalDateTime startTime,
LocalDateTime endTime,
List<GroupImageItem> images,
List<String> tags,
String description,
int maxParticipants,
LocalDateTime updatedAt
) { }


Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,4 @@ public AttendGroupV2Response left(Long userId, Long groupId) {

return AttendGroupV2Response.of(group, attendCount, membership);
}


}
Loading