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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ jobs:
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
S3_BUCKET_REGION: ${{ secrets.S3_BUCKET_REGION }}

services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
Comment on lines +33 to +42
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

테스트 프로파일의 Redis 설정과 CI의 Redis 서비스가 일치하는지 확인 필요

CI에서는 비밀번호 없이 localhost:6379로 접근 가능한 Redis를 띄웁니다. 애플리케이션의 테스트 프로파일(application-test.yml 등)이 동일한 호스트/포트/비번 없이 접속하도록 설정돼 있는지 확인하세요. 또한 일부 라이브러리는 spring.redis.* 프리픽스를, 현재 RedissonConfig는 spring.data.redis.* 프리픽스를 사용합니다. 설정 키 불일치 시 접속 실패가 발생할 수 있습니다.

다음 스크립트로 리소스 내 Redis 설정 키를 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# 리소스에서 Redis 관련 설정 키/값을 검색
echo "Searching for Redis properties under src/main/resources and test resources..."
fd -t f -a | grep -E 'src/.*/resources/.*\.(ya?ml|properties)$' | while read -r f; do
  echo "---- $f"
  rg -n -A 1 -e 'spring\.data\.redis\.(host|port|password|cluster\.nodes|ssl)' -e 'spring\.redis\.(host|port|password|cluster\.nodes|ssl)' "$f" || true
done

echo
echo "Checking for application-test profile existence..."
fd -t f -a | grep -E 'src/.*/resources/application(-test)?\.(ya?ml|properties)$' || true

Length of output: 1250


테스트 프로파일에 Redis 설정 누락됨 – CI 서비스와 동기화 필요

현재 CI는 비밀번호 없이 localhost:6379로 Redis를 실행하지만, 리포지토리의 설정 파일에는 spring.data.redis.* 또는 spring.redis.* 관련 키가 전혀 없습니다. 또한 RedissonConfig는 spring.data.redis.* 프리픽스를 사용 중이므로, 테스트 프로파일에 명시적인 설정이 반드시 필요합니다.

조치 사항:

  • src/test/resources/application-test.yml (필요 시 src/main/resources/application.yml) 에 아래와 같이 Redis 접속 정보를 추가
    spring:
      data:
        redis:
          host: localhost
          port: 6379
          # 비밀번호가 없으면 빈 문자열로 지정
          password: ""
  • 또는 애플리케이션 전반에서 사용하는 프리픽스가 spring.redis.* 인 경우, RedissonConfig 쪽을 spring.redis.* 로 통일

파일 위치:

  • src/test/resources/application-test.yml
  • (필요 시) src/main/resources/application.yml
🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 37-37: wrong indentation: expected 8 but found 10

(indentation)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 33 to 42: CI starts Redis on
localhost:6379 without auth but the project lacks test Redis properties and
RedissonConfig expects spring.data.redis.*; add explicit Redis settings to the
test profile or unify prefixes. Add spring.data.redis.host: localhost,
spring.data.redis.port: 6379 and spring.data.redis.password: "" to
src/test/resources/application-test.yml (or to
src/main/resources/application.yml if needed), or alternatively change
RedissonConfig to use spring.redis.* everywhere to match existing app
properties; ensure keys and prefix naming in config and code match exactly.

⚠️ Potential issue

Redis 서비스 추가는 적절합니다만, YAML 들여쓰기 경고와 포트 매핑 들여쓰기 수정이 필요합니다

  • YAMLlint가 지적한 대로 ports 항목의 리스트 들여쓰기가 과합니다. 깃허브 액션스 파서에 따라 무시될 수도 있지만, 린트 에러로 CI가 실패할 수 있으니 수정 권장합니다.
  • 기능 측면에서는 Redis 서비스 정의, healthcheck, 포트 매핑 모두 적절합니다. 테스트/빌드가 러너(호스트)에서 직접 Redis에 접속한다면 ports 매핑은 유지하는 것이 맞습니다.

아래처럼 ports의 리스트 들여쓰기를 2칸 줄여주세요.

       ports:
-          - 6379:6379
+        - 6379:6379
📝 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
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 37-37: wrong indentation: expected 8 but found 10

(indentation)

🤖 Prompt for AI Agents
.github/workflows/ci.yml around lines 33 to 42: YAML lint warns about excessive
indentation for the ports list; reduce the indentation of the ports list items
by two spaces so the dash for "- 6379:6379" is aligned under "ports:" as a
proper YAML sequence item, preserving the existing redis service, image,
healthcheck options and port mapping but fixing the list indentation to satisfy
linters and avoid CI failures.


steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'com.resend:resend-java:4.1.1'
implementation 'com.googlecode.libphonenumber:libphonenumber:9.0.9'
implementation 'net.javacrumbs.shedlock:shedlock-spring:6.9.2'
implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:6.9.2'
implementation 'org.redisson:redisson-spring-boot-starter:3.46.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public UserRegisterResponse register(UserRegisterRequest req) {
.build();
userAuthRepository.save(userAuth);

eventPublisher.publishEvent(new UserRegisteredEvent(userId, email));
eventPublisher.publishEvent(new UserRegisteredEvent(email));

return UserRegisterResponse.from(userId);
}
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/project/flipnote/common/config/RedissonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package project.flipnote.common.config;

import java.util.Arrays;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

@Configuration
public class RedissonConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Value("${spring.data.redis.password:}")
private String password;

@Value("${spring.data.redis.cluster.nodes:}")
private String clusterNodes;

Comment on lines +16 to +27
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

spring.redis.와 spring.data.redis. 동시 지원 + SSL 플래그 추가 제안

현재 모든 프로퍼티가 spring.data.redis.* 프리픽스를 사용합니다. Spring Boot와 서드파티 오토컨피그(예: Spring Data Redis, ShedLock의 RedisLockProvider)는 일반적으로 spring.redis.*를 기본으로 사용합니다. 양쪽 프리픽스를 모두 지원하면 설정 실수를 줄이고 호환성을 높일 수 있습니다. 또한 TLS 환경을 고려해 ssl 플래그도 주입받을 것을 권장합니다.

아래처럼 키 폴백과 ssl 플래그를 추가해 주세요.

-	@Value("${spring.data.redis.host}")
+	@Value("${spring.redis.host:${spring.data.redis.host}}")
 	private String host;

-	@Value("${spring.data.redis.port}")
+	@Value("${spring.redis.port:${spring.data.redis.port}}")
 	private int port;

-	@Value("${spring.data.redis.password:}")
+	@Value("${spring.redis.password:${spring.data.redis.password:}}")
 	private String password;

-	@Value("${spring.data.redis.cluster.nodes:}")
+	@Value("${spring.redis.cluster.nodes:${spring.data.redis.cluster.nodes:}}")
 	private String clusterNodes;
+
+	@Value("${spring.redis.ssl:${spring.data.redis.ssl:false}}")
+	private boolean ssl;
📝 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
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Value("${spring.data.redis.cluster.nodes:}")
private String clusterNodes;
@Value("${spring.redis.host:${spring.data.redis.host}}")
private String host;
@Value("${spring.redis.port:${spring.data.redis.port}}")
private int port;
@Value("${spring.redis.password:${spring.data.redis.password:}}")
private String password;
@Value("${spring.redis.cluster.nodes:${spring.data.redis.cluster.nodes:}}")
private String clusterNodes;
@Value("${spring.redis.ssl:${spring.data.redis.ssl:false}}")
private boolean ssl;
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/common/config/RedissonConfig.java around lines
16-27, the fields only read spring.data.redis.* properties; update the
injections to fall back to spring.redis.* equivalents and add an ssl flag: read
host from spring.data.redis.host with fallback to spring.redis.host, port from
spring.data.redis.port with fallback to spring.redis.port (use Integer if null),
password from spring.data.redis.password with fallback to spring.redis.password
(default empty), clusterNodes from spring.data.redis.cluster.nodes with fallback
to spring.redis.cluster.nodes (default empty), and add a boolean ssl injected
from spring.data.redis.ssl with fallback to spring.redis.ssl (default false);
ensure null-safe defaults so existing behavior remains unchanged.

@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();

if (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
"redis://" + addr)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
Comment on lines +32 to +47
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

TLS(rediss://) 및 주소 스킴 일관성 처리

현재 클러스터/싱글 모두 스킴이 하드코딩되거나(싱글: redis://), 클러스터는 기존 스킴만 보존하고 기본값은 redis://로 붙입니다. ssl 설정에 따라 rediss:// 스킴을 일관되게 적용할 수 있도록 스킴 변수를 도입하는 것을 권장합니다.

 	public RedissonClient redissonClient() {
 		Config config = new Config();
 
-		if (!clusterNodes.isBlank()) {
+		final String scheme = ssl ? "rediss" : "redis";
+
+		if (!clusterNodes.isBlank()) {
 			config.useClusterServers()
 				.addNodeAddress(
 					Arrays.stream(clusterNodes.split(","))
 						.map(String::trim)
 						.filter(s -> !s.isEmpty())
-						.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
-							"redis://" + addr)
+						.map(addr ->
+							(addr.startsWith("redis://") || addr.startsWith("rediss://"))
+								? addr
+								: (scheme + "://" + addr)
+						)
 						.toArray(String[]::new)
 				)
 				.setPassword(StringUtils.hasText(password) ? password : null);
 		} else {
 			config.useSingleServer()
-				.setAddress("redis://" + host + ":" + port)
+				.setAddress(scheme + "://" + host + ":" + port)
 				.setPassword(StringUtils.hasText(password) ? password : null);
 		}
📝 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 (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr -> addr.startsWith("redis://") || addr.startsWith("rediss://") ? addr :
"redis://" + addr)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress("redis://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
// introduce a scheme variable to pick redis:// vs rediss:// based on SSL
final String scheme = ssl ? "rediss" : "redis";
if (!clusterNodes.isBlank()) {
config.useClusterServers()
.addNodeAddress(
Arrays.stream(clusterNodes.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(addr ->
(addr.startsWith("redis://") || addr.startsWith("rediss://"))
? addr
: (scheme + "://" + addr)
)
.toArray(String[]::new)
)
.setPassword(StringUtils.hasText(password) ? password : null);
} else {
config.useSingleServer()
.setAddress(scheme + "://" + host + ":" + port)
.setPassword(StringUtils.hasText(password) ? password : null);
}
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/common/config/RedissonConfig.java around lines
32 to 47, introduce a scheme variable (e.g. String scheme = useSsl ? "rediss://"
: "redis://") and use it for both cluster and single-server address
construction: for cluster, preserve existing "redis://" or "rediss://" prefixes
but otherwise prefix with scheme; for single-server, build the address using
scheme + host + ":" + port; keep the existing password handling unchanged.
Ensure the code reads the SSL flag (or config property) to set useSsl and then
consistently applies the derived scheme for all addresses.


return Redisson.create(config);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package project.flipnote.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@Configuration
public class SchedulerConfig {
}
19 changes: 19 additions & 0 deletions src/main/java/project/flipnote/common/config/ShedLockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package project.flipnote.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;

@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
@Configuration
public class ShedLockConfig {

@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package project.flipnote.common.event;

public record UserRegisteredEvent(
Long userId,
String email
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode {
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "COMMON_001", "예기치 않은 오류가 발생했습니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST.value(), "COMMON_002", "입력값이 올바르지 않습니다.");
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST.value(), "COMMON_002", "입력값이 올바르지 않습니다."),
SERVICE_TEMPORARILY_UNAVAILABLE(HttpStatus.TOO_MANY_REQUESTS.value(), "COMMON_003", "요청이 많아 처리되지 않았습니다. 잠시 후 다시 시도해주세요.");

private final int status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public class GroupInvitationQueryController implements GroupInvitationQueryContr
@GetMapping("/groups/{groupId}/invitations")
public ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations(
@PathVariable("groupId") Long groupId,
@Min(0) @RequestParam(defaultValue = "0") int page,
@Min(1) @Min(30) @RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@AuthenticationPrincipal AuthPrinciple authPrinciple
) {
PageResponse<OutgoingGroupInvitationResponse> res
Expand All @@ -39,8 +39,8 @@ public ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoing

@GetMapping("/group-invitations")
public ResponseEntity<PageResponse<IncomingGroupInvitationResponse>> getIncomingInvitations(
@Min(0) @RequestParam(defaultValue = "0") int page,
@Min(1) @Min(30) @RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@AuthenticationPrincipal AuthPrinciple authPrinciple
) {
PageResponse<IncomingGroupInvitationResponse> res
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import project.flipnote.common.response.PageResponse;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.group.model.IncomingGroupInvitationResponse;
Expand All @@ -16,15 +18,15 @@ public interface GroupInvitationQueryControllerDocs {
@Operation(summary = "그룹 초대 보낸 목록 조회", security = {@SecurityRequirement(name = "access-token")})
ResponseEntity<PageResponse<OutgoingGroupInvitationResponse>> getOutgoingInvitations(
Long groupId,
int page,
int size,
@Min(0) int page,
@Min(1) @Max(30) int size,
AuthPrinciple authPrinciple
);

@Operation(summary = "그룹 초대 받은 목록 조회", security = {@SecurityRequirement(name = "access-token")})
ResponseEntity<PageResponse<IncomingGroupInvitationResponse>> getIncomingInvitations(
int page,
int size,
@Min(0) int page,
@Min(1) @Max(30) int size,
AuthPrinciple authPrinciple
);
}
16 changes: 14 additions & 2 deletions src/main/java/project/flipnote/group/entity/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
@SQLDelete(sql = "UPDATE app_groups SET deleted_at = now() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class Group extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand Down Expand Up @@ -59,6 +60,11 @@ public class Group extends BaseEntity {

private String imageUrl;

@Column(nullable = false)
@Min(1)
@Max(100)
private Integer memberCount;

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

Expand All @@ -79,11 +85,17 @@ private Group(
this.publicVisible = publicVisible;
this.maxMember = maxMember;
this.imageUrl = imageUrl;
this.memberCount = 1;
}

public void validateJoinable() {
if (maxMember < 1 || maxMember >= 100) {
throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER);
if (memberCount >= maxMember) {
throw new BizException(GroupErrorCode.GROUP_IS_ALREADY_MAX_MEMBER);
}
}

public void increaseMemberCount() {
validateJoinable();
memberCount++;
}
}
36 changes: 35 additions & 1 deletion src/main/java/project/flipnote/group/entity/GroupInvitation.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package project.flipnote.group.entity;

import java.time.LocalDateTime;
import java.util.Objects;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -17,12 +20,14 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import project.flipnote.common.entity.BaseEntity;
import project.flipnote.common.exception.BizException;
import project.flipnote.group.exception.GroupInvitationErrorCode;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(
name = "group_invitation",
name = "group_invitations",
indexes = {
@Index(name = "idx_group_invitee_user", columnList = "group_id, invitee_user_id, status"),
@Index(name = "idx_group_invitee_email", columnList = "group_id, invitee_email, status"),
Expand All @@ -36,6 +41,8 @@
)
public class GroupInvitation extends BaseEntity {

private static final long DEFAULT_EXPIRATION_DAYS = 7L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -55,16 +62,43 @@ public class GroupInvitation extends BaseEntity {
@Column(nullable = false)
private GroupInvitationStatus status;

@Column(nullable = false)
private LocalDateTime expiredAt;

@Builder
public GroupInvitation(Group group, Long inviterUserId, Long inviteeUserId, String inviteeEmail) {
this.group = group;
this.inviterUserId = inviterUserId;
this.inviteeUserId = inviteeUserId;
this.inviteeEmail = inviteeEmail;
this.status = GroupInvitationStatus.PENDING;
this.expiredAt = LocalDateTime.now().plusDays(DEFAULT_EXPIRATION_DAYS);
}

public void respond(GroupInvitationStatus status) {
this.status = status;
}

public void validateNotExpired() {
if (isExpired()) {
throw new BizException(GroupInvitationErrorCode.INVITATION_NOT_FOUND);
}
}

public boolean isExpired() {
if (this.status == GroupInvitationStatus.EXPIRED) {
return true;
}
if (this.status != GroupInvitationStatus.PENDING) {
return false;
}
return this.expiredAt.isBefore(LocalDateTime.now());
}

public GroupInvitationStatus getStatus() {
if (isExpired()) {
return GroupInvitationStatus.EXPIRED;
}
return status;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package project.flipnote.group.entity;

public enum GroupInvitationStatus {
PENDING, ACCEPTED, REJECTED
PENDING, ACCEPTED, REJECTED, EXPIRED;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package project.flipnote.group.exception;


import org.springframework.http.HttpStatus;

import lombok.Getter;
Expand All @@ -12,7 +11,9 @@
public enum GroupErrorCode implements ErrorCode {
GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_002", "그룹이 존재하지 않습니다."),
INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_001", "최대 인원 수는 1 이상 100 이하여야 합니다."),
USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_003", "그룹에 유저가 존재하지 않습니다.");
USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_003", "그룹에 유저가 존재하지 않습니다."),
GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_004", "그룹 정원이 가득 찼습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_005", "이미 그룹 회원입니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package project.flipnote.group.exception;


import org.springframework.http.HttpStatus;

import lombok.Getter;
Expand All @@ -13,8 +12,7 @@ public enum GroupInvitationErrorCode implements ErrorCode {
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 그룹 회원입니다.");
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");

Comment on lines 12 to 16
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

만료된 초대(Expired) 시나리오용 에러 코드를 추가하는 것이 명확합니다

초대 만료 로직이 도입되었다면, 만료 케이스를 NOT_FOUND로 뭉뚱그리기보다 전용 에러코드로 구분하는 편이 API 소비자 관점에서 명확합니다.

아래와 같이 초대 만료 전용 코드를 추가하는 것을 제안합니다.

 public enum GroupInvitationErrorCode implements ErrorCode {
   ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
   NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
   INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
+  INVITATION_EXPIRED(HttpStatus.GONE, "GROUP_INVITATION_005", "만료된 초대입니다."),
   CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
  • 상태값은 410 GONE을 권장합니다. 클라이언트가 “만료됨”을 명확히 처리할 수 있습니다.
  • 코드값은 기존 005가 비어 있으므로 재사용을 제안합니다.
📝 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
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다."),
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_INVITATION_005", "이미 그룹 회원입니다.");
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
public enum GroupInvitationErrorCode implements ErrorCode {
ALREADY_INVITED(HttpStatus.CONFLICT, "GROUP_INVITATION_001", "이미 초대된 사용자입니다."),
NO_INVITATION_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_INVITATION_002", "해당 그룹에 초대할 권한이 없습니다."),
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_INVITATION_003", "유효하지 않은 초대입니다."),
INVITATION_EXPIRED(HttpStatus.GONE, "GROUP_INVITATION_005", "만료된 초대입니다."),
CANNOT_INVITE_SELF(HttpStatus.BAD_REQUEST, "GROUP_INVITATION_004", "본인을 초대할 수 없습니다.");
🤖 Prompt for AI Agents
In src/main/java/project/flipnote/group/exception/GroupInvitationErrorCode.java
around lines 12 to 16, add a dedicated enum constant for expired invitations
instead of folding it into NOT_FOUND: declare
EXPIRED_INVITATION(HttpStatus.GONE, "GROUP_INVITATION_005", "초대가 만료되었습니다.") (use
HTTP 410 GONE and the unused code 005), place it with the other constants and
ensure commas/semicolon around the enum list remain correct so the enum
compiles.

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ public class UserRegisteredEventListener {
)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void HandleUserRegisteredEvent(UserRegisteredEvent event) {
groupInvitationService.acceptPendingInvitationsOnRegister(event.userId(), event.email());
groupInvitationService.acceptPendingInvitationsOnRegister(event.email());
}

@Recover
public void recover(Exception ex, UserRegisteredEvent event) {
log.error("회원가입 후속 처리 예외 발생: userId={}, email={}", event.userId(), event.email(), ex);
log.error("회원가입 후속 처리 예외 발생: email={}", event.email(), ex);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package project.flipnote.group.model;

public enum GroupInvitationStatus {
PENDING, ACCEPTED, REJECTED;
PENDING, ACCEPTED, REJECTED, EXPIRED;

public static GroupInvitationStatus from(project.flipnote.group.entity.GroupInvitationStatus status) {
if (status == null) {
Expand All @@ -12,6 +12,7 @@ public static GroupInvitationStatus from(project.flipnote.group.entity.GroupInvi
case PENDING -> PENDING;
case ACCEPTED -> ACCEPTED;
case REJECTED -> REJECTED;
case EXPIRED -> EXPIRED;
default -> throw new IllegalArgumentException("Unknown GroupInvitationStatus: " + status);
};
}
Expand Down
Loading
Loading