Conversation
|
Caution Review failedThe pull request is closed. Walkthrough과제/제출 도메인을 전면 재구성했다. 새 엔티티·레포지토리·서비스·컨트롤러를 도입하고, 파일/S3 연동과 JPA 감사(Auditing) 기반 공통 BaseEntity를 추가했다. CurrentUser 리졸버·CORS·웹 설정을 갱신했고, 빌드/의존성(QueryDSL, S3)과 환경설정을 수정했다. 구(legacy) 코드와 DTO/컨트롤러는 대거 제거됐다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Ctrl as AssignmentQueryController
participant Cmd as AssignmentQueryService
participant CRS as ClassRoomRepository
participant USR as UserService
participant ARepo as AssignmentRepository
participant SubQ as SubmissionQueryService
participant CRU as ClassRoomUserRepository
note over User,Ctrl: 과제 생성 요청 (POST /api/assignments)
User->>Ctrl: createAssignment(request, @CurrentUser userId)
Ctrl->>Cmd: save(userId, request)
Cmd->>USR: findById(userId)
Cmd->>CRS: findByClassRoomId(request.classId)
Cmd->>ARepo: save(Assignment.create(...))
Cmd-->>Ctrl: Assignment
Ctrl->>SubQ: assignToAllStudentsInClassroom(classId, assignment)
SubQ->>CRU: findAllStudentsByClassRoomId(classId)
SubQ->>ARepo: (read assignment)
SubQ->>ARepo: (no-op persist assignment)
SubQ-->SubQ: Submission 생성 리스트
SubQ-->>Ctrl: 완료
Ctrl-->>User: 200 OK (assignmentId)
sequenceDiagram
autonumber
actor User
participant Ctrl as SubmissionCommandController
participant Svc as SubmissionCommandService
participant SARepo as SubmissionAttachmentRepository
participant FS as FileService
note over User,Ctrl: 제출 첨부 다운로드 (GET /api/submissions/{id}/download)
User->>Ctrl: download(@CurrentUser userId, submissionAttachmentId)
Ctrl->>Svc: findsubmissionAttachmentByIdOrThrow(id)
Svc->>SARepo: findById(id)
SARepo-->>Svc: SubmissionAttachment(value=path,...)
Svc->>FS: downloadFile(value)
FS-->>Svc: Resource
Ctrl-->>User: 200 OK (Resource, Content-Disposition)
sequenceDiagram
autonumber
participant Web as Spring MVC
participant Resolver as CurrentUserArgumentResolver
participant JWT as JWTUtil
participant Ctrl as Any *@CurrentUser Controller
Web->>Resolver: resolveArgument(@CurrentUser Long)
Resolver->>Web: HttpServletRequest
Resolver->>JWT: getToken(request)
JWT-->>Resolver: token
Resolver->>JWT: getUserId(token)
JWT-->>Resolver: userId
Resolver-->>Web: userId(Long)
Web->>Ctrl: invoke(controllerMethod, userId)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (2)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 29
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/main/resources/application.yaml (2)
21-21: 하드코딩된 JWT Secret 노출 — 즉시 회수/교체 필요애플리케이션 설정에 실제 시크릿이 커밋되어 있습니다. 보안 사고로 간주하고 즉시 시크릿을 회수/교체하고, 환경 변수로 주입하도록 변경하세요.
- secret: vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb + secret: ${JWT_SECRET}추가 권고:
- 노출된 기존 토큰 전부 무효화(블랙리스트 혹은 키 롤오버).
- 로컬 전용
application-local.yaml에만 테스트 키를 두고 Git에 커밋하지 않기(.gitignore + sample은application-local.example.yaml).
31-31: 환경 변수 키 오타: ${GOOGlE_REDIRECT_URI} → ${GOOGLE_REDIRECT_URI}소문자 l(엘)과 대문자 I(아이) 오타로 보입니다. 현재대로면 값이 주입되지 않아 OAuth 콜백 실패가 납니다.
- redirect-uri: ${GOOGlE_REDIRECT_URI} + redirect-uri: ${GOOGLE_REDIRECT_URI}docker-compose.yaml (1)
18-21: DB 자격 증명 하드코딩(비공개 정보 노출 위험)
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB가 평문으로 커밋되어 있습니다. 퍼블릭/사내 저장소 여부와 무관하게 즉시 교체(rotate)하고.env기반으로 주입하세요. 저장소에는.env.example만 커밋하세요.적용안:
environment: - POSTGRES_USER: clue1234 - POSTGRES_PASSWORD: clue52025 - POSTGRES_DB: clue + POSTGRES_USER: ${POSTGRES_USER:-clue} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} + POSTGRES_DB: ${POSTGRES_DB:-clue} + env_file: + - ./.env추가 권고:
- 현재 값이 외부 환경(테스트/스테이징/프로덕션 등)에서 사용 중이면 즉시 비밀번호/사용자 교체.
- Git 기록에 남은 비밀은 더 이상 비밀이 아닙니다. 필요 시 히스토리 정리 또는 키/패스워드 폐기 및 재발급 권장.
build.gradle (1)
67-71: AWS SDK v1/v2 혼용 및 Spring Cloud AWS(2.2.6)와 Spring Boot 3.x 비호환
- 코드에서는 v1(AmazonS3Client)을 사용 중인데, 의존성은 v2(
software.amazon.awssdk:s3)도 함께 포함되어 있습니다.org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE는 Spring Boot 2.x 계열용으로 3.x와 비호환 사례가 잦습니다.단일 스택으로 정리하세요. 권장안은 SDK v2 단독 사용입니다.
다음과 같이 최소 정리안을 제안합니다(먼저 컴파일·코드 변경 필요):
- implementation 'software.amazon.awssdk:s3:2.32.1' - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // AWS SDK v2만 사용 + implementation 'software.amazon.awssdk:s3:2.32.1'그리고 S3Config/FileService는 v2 API(S3Client/S3AsyncClient)로 마이그레이션하세요. 예시(파일 외부 참고용):
// S3Config (v2) @Bean software.amazon.awssdk.services.s3.S3Client s3Client( @Value("${cloud.aws.region.static}") String region, @Value("${cloud.aws.credentials.access-key}") String accessKey, @Value("${cloud.aws.credentials.secret-key}") String secretKey ) { return software.amazon.awssdk.services.s3.S3Client.builder() .region(software.amazon.awssdk.regions.Region.of(region)) .credentialsProvider(() -> software.amazon.awssdk.auth.credentials.AwsBasicCredentials.create(accessKey, secretKey)) .build(); }대안(권장 X): v1 유지 시에는 v2 의존성 제거 및 Spring Cloud AWS를 3.x(io.awspring.cloud)로 올리는 방안을 검토하세요(버전은 팀 정책에 맞춰 확인 필요).
src/main/java/hello/cluebackend/domain/classroomuser/domain/repository/ClassRoomUserRepository.java (1)
6-6: @param 잘못된 패키지 임포트 – Spring Data 동작 불가
io.lettuce.core.dynamic.annotation.Param는 Spring Data JPA가 인식하지 않습니다. 런타임에 파라미터 바인딩 실패를 유발합니다. Spring Data의org.springframework.data.repository.query.Param로 교체해야 합니다.-import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param;
🧹 Nitpick comments (87)
src/main/resources/application.yaml (1)
5-6: 환경별 DB URL: 하드코딩 대신 환경 변수 기본값 문법으로 통합하세요로컬 기본값과 배포 환경 변수를 동시에 커버하려면 Spring placeholder의 기본값 문법을 사용하면 됩니다. 주석 라인 유지 대신 한 줄로 정리하는 편이 안전하고 깔끔합니다.
- url: jdbc:postgresql://localhost:5432/clue -# url: ${POSTGRES_URL} + url: ${POSTGRES_URL:jdbc:postgresql://localhost:5432/clue}로컬/CI/배포(예: Docker, Kubernetes)에서 각각
POSTGRES_URL이 적절히 주입되는지 확인해주세요.docker-compose.yaml (6)
2-8: spring 서비스가 통째로 주석 처리됨 — compose profiles로 비활성/활성 제어 권장주석으로 남겨두면 구성이 쉽게 드리프트되고, 로컬/CI에서 의도치 않게 서비스가 빠진 상태가 됩니다. compose의 profiles를 사용해 기본 비활성 + 필요 시 활성화하도록 바꾸면 주석 없이도 의도를 명확히 표현할 수 있습니다. 예)
docker compose --profile app up -d.아래처럼 교체하는 안을 제안합니다. 의도가 “스프링 컨테이너를 당분간 사용하지 않음”이라면 이 블록을 완전히 제거하고 README에 사용 지침을 남기는 편이 더 낫습니다.
-# spring: -# build: . -# container_name: paletto -# networks: -# - web-net -# ports: -# - 8080:8080 + spring: + build: . + container_name: paletto + networks: + - web-net + ports: + - "8080:8080" + profiles: ["app"] + depends_on: + - postgres + - redis
26-26: Redis 이미지 태그 고정 필요
image: redis는 moving tag(latest)로, 재빌드 시 예기치 않은 버전 업그레이드를 유발합니다. 메이저/마이너를 고정하세요.- image: redis + image: redis:7-alpine
10-23: Postgres에 healthcheck 추가 권장 (spring depends_on 안정화 전제조건)
depends_on을 신뢰하려면 서비스가 “healthy” 상태를 노출해야 합니다. 현재는 재시도 로직이 없고, 스프링이 DB 준비 전 접속 시 실패할 수 있습니다.postgres: image: postgres:15 container_name: my_postgres restart: always networks: - web-net ports: - "5432:5432" environment: POSTGRES_USER: ${POSTGRES_USER:-clue} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} POSTGRES_DB: ${POSTGRES_DB:-clue} volumes: - ./postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-clue} -d ${POSTGRES_DB:-clue}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s메모: 위 healthcheck를 쓰려면 spring 서비스의
depends_on을condition: service_healthy로 강화하는 것을 권장합니다(별도 코멘트 참고).
25-31: Redis에도 healthcheck와 재시작 정책 정렬 권장현재 Redis는 restart 정책이 없고 healthcheck도 없습니다. Postgres와 일관성 있게 설정하여 초기 기동 안정성을 높이세요.
redis: - image: redis:7-alpine + image: redis:7-alpine container_name: my_redis networks: - web-net ports: - "6379:6379" + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5
12-12: container_name 하드코딩은 로컬 프로젝트 병행 실행을 방해
container_name을 고정하면 동일 머신에서 다른 프로젝트 이름으로 병행 실행이 어려워지고 CI에서도 충돌을 유발할 수 있습니다. 가능하면 제거하고 Docker가project_service_index규칙으로 이름을 생성하게 두세요.- container_name: my_postgres + # container_name: my_postgres- container_name: my_redis + # container_name: my_redisAlso applies to: 27-27
2-8: 스프링 서비스의 depends_on 개선(healthcheck 기반) — 선택 사항위에서 제안한 healthcheck를 도입했다면, 스프링 서비스의
depends_on도 아래처럼 보강하세요.spring: build: . container_name: paletto networks: - web-net ports: - "8080:8080" profiles: ["app"] - depends_on: - - postgres - - redis + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthyAlso applies to: 10-23, 25-31
src/main/java/hello/cluebackend/global/common/annotation/CurrentUser.java (2)
8-11: 주석/메타 애너테이션 보강 권장사용처 문서화와 IDE/Hover 가시성을 위해
@Documented추가 및 간단한 Javadoc을 제안드립니다.package hello.cluebackend.global.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.annotation.Documented; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) +@Documented public @interface CurrentUser { }
8-11: 파라미터 타입 제약/가이드 문서화현재 리졸버가
Long userId만 주입한다면, 다른 타입으로의 사용을 방지/안내할 가이드가 필요합니다. 간단한 Javadoc에 허용 타입과 동작(예: “JWT의 sub를 Long으로 파싱”)을 명시해 주세요.
컨트롤러 샘플도 README나 API 문서에 포함하면 온보딩에 도움이 됩니다.src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAttachmentUrlDto.java (2)
3-5: 입력 값 검증 추가 권장(@url, @notblank, 길이 제한)URL 형식/길이 검증이 없어 잘못된 입력 저장 가능성이 있습니다. Hibernate Validator 사용 시 다음과 같이 보강하세요.
Spring Boot 3+(Jakarta) 사용 시:
package hello.cluebackend.domain.submission.api.dto.request; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.URL; + public record SubmissionAttachmentUrlDto( - String url + @NotBlank + @Size(max = 2048) + @URL(protocol = "http", regexp = ".*", message = "유효한 URL 형식이어야 합니다.") + String url ) { }Spring Boot 2.x( javax.validation )를 사용 중이라면
jakarta.*를javax.*로 바꿔 적용하세요.
3-5: 보안 고려(미래 SSRF/내부망 유출 대비 가드)현재는 저장만 하더라도, 이후 서버가 해당 URL을 호출하는 기능이 추가되면 SSRF 위험이 생깁니다.
- 사설망/로컬호스트/메타데이터 IP 대역 차단(예: 169.254.169.254 등) 및 허용 도메인 화이트리스트 설계를 미리 고려해 주세요.
src/main/java/hello/cluebackend/ClueBackendApplication.java (1)
5-11: 보안 컨텍스트 연동 전제로 import 정리SecurityContext 기반으로 전환 시
UUID,Optional외에SecurityContextHolder,Authentication,UserDetailsimport가 필요합니다. 반대로 랜덤 UUID 전략 제거로UUID는 더 이상 사용하지 않을 수 있습니다. 적용하면서 import를 정리해 주세요.build.gradle (2)
25-36: 중복 의존성 정리(web/webflux, security, oauth2-client, validation, lombok)중복 선언으로 클래스패스 비대·혼선 우려가 있습니다. web vs webflux는 런타임 충돌 가능성도 있습니다. 사용 의도가 명확하지 않다면 MVC(web) 또는 WebFlux 중 하나만 선택하세요. Lombok은 compileOnly+annotationProcessor 조합만 두고 implementation은 제거하세요.
다음 정리안을 제안합니다:
- implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.projectlombok:lombok:1.18.30' - implementation 'org.springframework.boot:spring-boot-starter-webflux' + // 필요 시에만 WebFlux 사용. 현재는 제거 권장. + // implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-validation' + // 중복 제거 - annotationProcessor 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-web' + // 위에서 이미 선언됨. 중복 제거. - compileOnly 'org.projectlombok:lombok' + compileOnly 'org.projectlombok:lombok'중복 제거 후, 테스트에서 WebFlux 의존이 필요하면 reactor-test만 유지해도 충분한지 검토 바랍니다.
Also applies to: 53-56
79-87: 생성 코드를 소스 트리(src/main/generated)로 내보내는 방식은 비권장빌드 산출물이 소스 디렉터리를 오염합니다. build 디렉터리 하위로 변경을 권장합니다.
권장 수정:
-def querydslSrcDir = 'src/main/generated' +def querydslSrcDir = "$buildDir/generated/querydsl" -clean { - delete file(querydslSrcDir) -} +clean { delete file(querydslSrcDir) } -tasks.withType(JavaCompile).configureEach { - options.generatedSourceOutputDirectory = file(querydslSrcDir) -} +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory = file(querydslSrcDir) +}추가로, 소스셋에 경로를 포함시키면 IDE 인식이 원활합니다(파일 외부 참고용):
sourceSets { main { java { srcDir querydslSrcDir } } }src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAssignmentAttachmentDto.java (1)
3-4: 빈 record DTO는 실사용 가치가 낮습니다 — 필드 추가 또는 제거 검토현재 빈 타입으로는 검증/직렬화/문서화에 의미가 없습니다. 과제 첨부(파일/URL 등) 설계에 맞춰 필드를 정의해 주세요.
예시:
-public record SubmissionAssignmentAttachmentDto() { -} +public record SubmissionAssignmentAttachmentDto( + String filePath, + String originalFileName, + Long attachmentId +) { }URL 전용 DTO가 별도로 있다면(file/URL 분리), 이 DTO는 파일 메타데이터 중심으로 최소 필드만 유지해도 됩니다. 사용처(컨트롤러/서비스)와 일치하는지 확인 부탁드립니다.
src/main/java/hello/cluebackend/domain/file/exception/S3FileNotFoundException.java (1)
5-7: AWS SDK 예외 상속 대신 도메인 예외(RuntimeException)로 디커플링 권장
AmazonS3Exception상속은 도메인 계층을 AWS 구현에 결합시킵니다. 컨트롤러 단에서 404 매핑만 필요하다면 도메인 예외로 충분합니다.다음과 같이 변경 제안:
-import com.amazonaws.services.s3.model.AmazonS3Exception; - -public class S3FileNotFoundException extends AmazonS3Exception { - public S3FileNotFoundException(String message) { super(message); } -} +public class S3FileNotFoundException extends RuntimeException { + public S3FileNotFoundException(String message) { super(message); } + public S3FileNotFoundException(String message, Throwable cause) { super(message, cause); } +}그리고 사용처(FileService)에서 404만 캐치해 변환:
try { // getObject... } catch (com.amazonaws.services.s3.model.AmazonS3Exception e) { if (e.getStatusCode() == 404) { throw new S3FileNotFoundException("S3 object not found: " + filePath, e); } throw e; }추가로
@ControllerAdvice로S3FileNotFoundException을ResponseStatus(HttpStatus.NOT_FOUND)에 매핑하면 API 일관성이 좋아집니다.src/main/java/hello/cluebackend/domain/assignment/api/dto/response/SubmissionCheck.java (1)
10-16: boolean 필드명isSubmitted는 Lombok/Jackson 프로퍼티 네이밍 혼선을 유발할 수 있습니다Lombok가 생성하는 getter가
isSubmitted()인지getIsSubmitted()인지 도구별 상이할 수 있어 직렬화 키가 혼동될 수 있습니다. 필드를submitted로 두고 getter를isSubmitted()로 만드는 방식이 안전합니다. 또한 UserDto에classCode가 있으므로classNumberGrade명명은 일관성 측면에서 아쉽습니다.다음과 같이 정리 제안:
- private int classNumberGrade; + private int classCode; - private boolean isSubmitted; + private boolean submitted;Jackson 호환을 확실히 하려면:
// 선택: 명시적 제어 @com.fasterxml.jackson.annotation.JsonProperty("isSubmitted") public boolean isSubmitted() { return submitted; }또는 DTO를 record로 단순화하는 방법도 있습니다:
public record SubmissionCheck( String userName, int classCode, Long submissionId, boolean submitted, LocalDateTime submittedAt ) {}변경 시 호출부/프론트와의 계약 영향 확인 바랍니다.
src/main/java/hello/cluebackend/domain/submission/domain/BaseEntity.java (2)
19-31: 감사(Auditing) 컬럼 제약을 명시해 무결성과 스키마 일관성 확보감사 필드는 영속 시 항상 채워지고(생성일/생성자), 수정 시 갱신되어야 하므로 null/업데이트 제약을 컬럼에 반영해 주세요. 또한 createdBy/lastModifiedBy 길이를 제한해 인덱싱 및 저장소 효율을 높일 수 있습니다.
아래처럼 제약을 추가하는 것을 제안합니다.
@CreatedDate - @Column + @Column(nullable = false, updatable = false) private LocalDateTime createdDate; @LastModifiedDate + @Column(nullable = false) private LocalDateTime lastModified; @CreatedBy - @Column(updatable = false) + @Column(updatable = false, length = 64) private String createdBy; @LastModifiedBy + @Column(length = 64) private String lastModifiedBy;
15-18: 두 개의 BaseEntity 중복 선언 — 공용 모듈로 통합 권장assignment/submission 두 모듈에 동일한 BaseEntity가 존재합니다. 장기적으로 스키마/도메인 불일치를 야기할 수 있어 global/common 등 공용 패키지로 통합하는 편이 안전합니다. 한 곳만 변경해도 전체 영향이 반영되도록 단일 소스로 관리하는 것을 권장합니다.
원하시면 리팩터링 플랜(패키지 이동, import 정리, 하위 엔티티 상속 변경)에 대한 커밋 가이드를 드리겠습니다.
src/main/java/hello/cluebackend/global/common/response/ResponseCode.java (1)
1-46: ResponseCode.java가 현재 코드베이스에서 전혀 참조되지 않음검증 결과:
ResponseCode및ApiResponse관련 모든 호출이 전부 주석 처리되어 있어, 실제로 활성화된 코드에서 사용되는 곳이 없습니다.rg -nP '\bResponseCode\b' src/main/java및rg -nP 'ApiResponse<|ResponseEntity<\s*ApiResponse' src/main/java검사 시 모두 주석 라인만 탐지되었습니다.이에 따른 권장 조치:
- 더 이상 사용 계획이 없다면 해당 파일(
src/main/java/hello/cluebackend/global/common/response/ResponseCode.java)을 삭제하여 유지보수 잡음을 줄이세요.- 향후 사용 예정이라면 주석을 해제하고,
ApiResponse와 연계된 빌드 메서드(buildResponse)를 활성화하여 실제 구현으로 포함하세요.src/main/java/hello/cluebackend/domain/assignment/domain/BaseEntity.java (2)
19-31: 여기도 동일 — 감사 컬럼 제약 추가로 스키마 일관성 강화submission 쪽과 동일한 이유로 제약을 명시하는 것을 권장합니다.
@CreatedDate - @Column + @Column(nullable = false, updatable = false) private LocalDateTime createdDate; @LastModifiedDate + @Column(nullable = false) private LocalDateTime lastModified; @CreatedBy - @Column(updatable = false) + @Column(updatable = false, length = 64) private String createdBy; @LastModifiedBy + @Column(length = 64) private String lastModifiedBy;
15-18: BaseEntity 중복 통합 제안(assignment/submission)두 BaseEntity를 공용 패키지로 이동해 단일 소스로 관리하는 것을 권장합니다. 마이그레이션 시 충돌을 줄이려면 먼저 공용 클래스를 추가 → 하위 엔티티 상속 전환 → 중복 클래스 제거 순으로 진행하세요.
희망하시면 단계별 커밋/PR 분할 가이드를 제공하겠습니다.
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java (5)
17-19: 연관관계 필수 여부 명시 및 FK 인덱스 추가첨부가 과제 없이 존재할 수 없다면 not-null 제약을 컬럼과 매핑에 반영하고, 조회 성능을 위해 인덱스를 추가해 주세요.
-@Table(name = "assignment_attachment") +@Table( + name = "assignment_attachment", + indexes = { + @Index(name = "idx_assignment_attachment_assignment_id", columnList = "assignment_id") + } +) ... -@ManyToOne(fetch = FetchType.LAZY) -@JoinColumn(name="assignment_id") +@ManyToOne(fetch = FetchType.LAZY, optional = false) +@JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment;
21-25: Enum 타입 네이밍 컨벤션 준수(FileType)현재 타입명이 소문자 시작(fileType)으로 보입니다. Java 컨벤션과 일관성을 위해 Enum을 FileType으로 변경하는 것을 권장합니다.
-@Enumerated(EnumType.STRING) -private fileType type; +@Enumerated(EnumType.STRING) +private FileType type;추가 작업:
- enum 선언 파일명을 FileType.java로 변경하고
public enum FileType { FILE, URL }형태로 노출- 사용처 전반 import/참조 업데이트
26-29: 'value' 컬럼 네이밍/길이 보완(예약어 회피 및 URL/S3 Key 길이 대응)'value'는 일부 DB에서 혼동 여지가 있고(문법 키워드와 충돌 가능성), URL/S3 Key는 255자를 넘길 수 있어 길이 확장이 필요합니다. 컬럼명을 명확히 하고 길이를 넉넉히 잡아 주세요.
-// 실제 파일이면 S3 Key, URL이면 링크 -@Column(nullable = false) -private String value; +// 실제 파일이면 S3 Key, URL이면 링크 +@Column(name = "resource_value", nullable = false, length = 2048) +private String value;
30-33: 메타데이터 컬럼 길이 지정파일명/Content-Type은 길이 제약을 명시해 두는 편이 안전합니다.
-// 파일일 경우 메타데이터 -private String originalFileName; -private String contentType; -private Long size; +// 파일일 경우 메타데이터 +@Column(length = 255) +private String originalFileName; +@Column(length = 255) +private String contentType; +private Long size;
8-12: 엔티티 + @builder 조합에서 @AllArgsConstructor는 보수적으로 제거 권장@AllArgsConstructor는 식별자/연관관계를 포함한 불완전 객체 생성을 유도할 수 있습니다. 빌더만 허용하거나 필요한 필드만 받는 생성자로 한정하는 편이 안전합니다.
-@AllArgsConstructor -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class AssignmentAttachment extends BaseEntity { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AssignmentAttachment extends BaseEntity {src/main/java/hello/cluebackend/domain/assignment/api/dto/request/AssignmentAttachmentDto.java (2)
3-5: 입력 검증 추가(@notblank, @url)URL 첨부 전용 DTO라면 유효성 검증을 바로 부여하는 것이 깔끔합니다.
+import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.URL; + public record AssignmentAttachmentDto( - String url + @NotBlank @URL String url ) {}
1-5: 동일 단순명 DTO 혼재 가능성 점검(이름 충돌/혼동 방지)기존에 presentation/dto 경로에 같은 단순명 AssignmentAttachmentDto가 존재합니다(성격: 응답/내부용). 패키지로 구분되지만 실무에서 혼동이 잦아, 요청 전용이라면 AssignmentAttachmentUrlRequest 등으로 명시적인 네이밍을 고려해 주세요.
원하시면 사용처 자동 치환 및 import 정리를 위한 스크립트를 제공하겠습니다.
src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionAttachmentRepository.java (2)
3-3: 불필요한 import 정리
Assignment는 사용되지 않습니다. 정리해 주세요.-import hello.cluebackend.domain.assignment.domain.Assignment;
11-14: LGTM – 기본 리포지토리 메서드 적합
findAllBySubmission(Submission submission)메서드 시그니처와 반환 타입이 도메인 모델에 잘 매칭됩니다.많은 첨부 삭제/정리 시 유용한 아래 메서드 추가를 고려해 보세요(선택):
public interface SubmissionAttachmentRepository extends JpaRepository<SubmissionAttachment, Long> { List<SubmissionAttachment> findAllBySubmission(Submission submission); + void deleteAllBySubmission(Submission submission); }src/main/java/hello/cluebackend/domain/classroomuser/domain/repository/ClassRoomUserRepository.java (1)
29-31: 신규 메서드들은 유용함 + ID 기반 exists 메서드 추가 제안
findAllByClassRoom과existsByClassRoomAndUser는 서비스 요구사항에 적합합니다. 성능 최적화를 위해 엔티티 로딩 없이 ID만으로 존재 여부를 판단하는 메서드도 함께 제공하면 좋습니다(선택).List<ClassRoomUser> findAllByClassRoom(ClassRoom classRoom); boolean existsByClassRoomAndUser(ClassRoom classRoom, UserEntity user); + boolean existsByClassRoom_ClassRoomIdAndUser_UserId(Long classRoomId, Long userId);src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java (2)
9-12: 필드 의미 명확화(네이밍) 제안
value는 의미가 모호합니다(S3 키/URL/텍스트 중 무엇인지 불명). API 계약 명확성을 위해uri또는location등 구체적 이름 사용을 고려하세요.- String value, + String uri,해당 필드가 실제로 어떤 값을 담는지(예: S3 Object Key, 프리사인드 URL, 원문 텍스트) 명시 부탁드립니다. 필요 시 Swagger 스키마 설명도 추가 가능합니다.
3-8: Enum 네이밍 컨벤션:fileType→FileType리네임 권장Java 타입은 대문자 로워캡(Capitalized CamelCase)로 시작하는 것이 컨벤션입니다.
현재 프로젝트에는 아래 두 위치에 모두 소문자fileTypeenum이 선언되어 있어, 사용 시 오타 실수나 가독성 저하로 이어질 수 있습니다.
가능한 배포 사이클 중 여유가 있을 때, 아래 파일명을 포함한 전체 패키지에서fileType→FileType으로 일괄 리네임을 고려해주세요.
- src/main/java/hello/cluebackend/domain/submission/domain/fileType.java
- src/main/java/hello/cluebackend/domain/assignment/domain/fileType.java
예상 변경 예시:
- import hello.cluebackend.domain.assignment.domain.fileType; + import hello.cluebackend.domain.assignment.domain.FileType; … - fileType type, + FileType type,src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionRepository.java (3)
10-11: 패키지/레이어 명명 컨벤션 불일치 가능성다른 리포지토리들(
domain/.../domain/repository)과 달리 여기만.../submission/persistence패키지를 사용합니다. 팀 표준에 맞춰.../domain/repository로 통일을 검토하세요. 혼재되면 탐색성과 의존 방향 파악이 어려워집니다.
12-12: LGTM: 컬렉션 조회 메서드
List<Submission> findAllByAssignment(Assignment assignment)는 의도에 부합합니다. 후속 요구가 예상되면 페이징 버전(Page<Submission> findAllByAssignment(Assignment assignment, Pageable pageable))도 고려해보세요.
3-6: 불필요한 @repository 애너테이션
JpaRepository를 상속하는 인터페이스는 컴포넌트 스캔 대상이므로@Repository가 없어도 빈으로 등록됩니다. 유지해도 무방하나 팀 컨벤션에 따라 제거 가능.src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionAttachmentDto.java (2)
3-3: 타입명fileType는 자바 컨벤션 위반 —FileType로 정정 권장열거형/클래스는 PascalCase가 관례입니다. 현재처럼 소문자 타입명은 가독성과 정적 분석(예: Sonar)에서 지속적으로 경고를 유발합니다. enum 자체를
FileType으로 리네임하고, 전역 참조를 일괄 수정하는 것을 권장합니다.적용 예시:
-import hello.cluebackend.domain.submission.domain.fileType; +import hello.cluebackend.domain.submission.domain.FileType;
7-13: 필드 의미가 불분명:type-value조합 명확화 필요
type이 file/url을 의미하고value가 경로 또는 URL이라면, 필드명을 더 구체적으로(storageType,pathOrUrl) 하거나 JavaDoc을 추가해 계약을 명확히 하세요. API 소비자 혼선을 줄입니다.적용 예시(선호):
-public record SubmissionAttachmentDto( - FileType type, - String value, +public record SubmissionAttachmentDto( + FileType storageType, + String pathOrUrl, String originalFileName, String contentType, Long size ) { }필요하시다면 enum 리네임 및 전역 치환 스크립트까지 생성해드리겠습니다.
src/main/java/hello/cluebackend/domain/classroom/presentation/ClassRoomController.java (6)
3-3: 미사용 import 제거
AssignmentCommandService는 사용되지 않습니다. 정리하세요.-import hello.cluebackend.domain.assignment.application.AssignmentCommandService;
35-42: 토큰 추출 코드 및 주석 정리
getAllInfo에서token을 추출하지만 사용하지 않고, 주석된 코드가 남아 있습니다. 불필요한 연산/노이즈이므로 제거하세요.public ResponseEntity<?> getAllInfo(HttpServletRequest request, @PathVariable Long classId){ - String token = jwtUtil.getToken(request); -// Long userId = jwtUtil.getUserId(token); -// Role role = jwtUtil.getRole(token); - return ResponseEntity.ok(classRoomService.getAllInfo(classId)); }
45-58: 권한 실패 시 HTTP 400 대신 403 사용 + 반환 타입 단순화권한 부족은 클라이언트 입력 오류(400)가 아닌 인가 실패(403)입니다. 또한 바디 없는 응답에
ResponseEntity<HashMap<?,?>>는 부적절합니다.ResponseEntity<Void>로 단순화하세요.- public ResponseEntity<HashMap<?,?>> createClassRoom(@RequestBody ClassRoomDto classRoomDTO, HttpServletRequest request) { + public ResponseEntity<Void> createClassRoom(@RequestBody ClassRoomDto classRoomDTO, HttpServletRequest request) { String token = jwtUtil.getToken(request); Role role = jwtUtil.getRole(token); Long userId = jwtUtil.getUserId(token); if(role != Role.TEACHER) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(HttpStatus.FORBIDDEN); } try { classRoomService.createClassRoom(classRoomDTO, userId); - return new ResponseEntity<>(HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.OK); } catch (EntityNotFoundException e){ log.error(e.getMessage()); - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }전역 예외 처리(@ControllerAdvice)로
EntityNotFoundException→ 404,IllegalArgumentException→ 400을 일관 매핑하면 컨트롤러가 더 간결해집니다.
69-71: 로그 레벨 상향
joinClassRoom의 실패 분기는 사용자 행위 오류가 아니면 최소warn이 더 적절합니다. 디버그 레벨은 운영에서 묻힐 수 있습니다.- log.debug(e.getMessage()); + log.warn(e.getMessage());
76-86: 미사용 변수/주석 제거
findClassRoom에서role은 사용되지 않고, 접근 제어 주석도 그대로 남아 있습니다. 정리하세요.public ResponseEntity<ClassRoomDto> findClassRoom(@PathVariable Long classId, HttpServletRequest request) { String token = jwtUtil.getToken(request); - Role role = jwtUtil.getRole(token); - -// if(role != Role.TEACHER) { -// return new ResponseEntity<>(HttpStatus.BAD_REQUEST); -// } ClassRoomDto findClassRoomDto = classRoomService.findById(classId); return ResponseEntity.ok(findClassRoomDto); }
88-105: PATCH 인가 실패 코드 정정
updateClassRoom에서도 권한 실패 시 400이 아닌 403을 반환하세요.if(role != Role.TEACHER) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + return new ResponseEntity<>(HttpStatus.FORBIDDEN); }이 컨트롤러 전반에 @currentuser 리졸버(이번 PR에 추가된 것으로 보임)를 적용하면 매 요청마다 토큰 파싱을 반복하지 않아도 됩니다.
src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentResponseDto.java (1)
17-19: 필드명 간소화 제안:xAssignmentResponseDtos→attachmentsDTO의 필드명은 외부 계약(API 스펙)과 직렬화 필드명을 동시에 결정합니다. 접두어
x는 특별한 의미가 없고, 간결한attachments가 가독성과 유지보수성에 모두 개선을 가져옵니다.
아래 위치를 일괄 변경해 주세요:
- AssignmentResponseDto.java
• 생성자 파라미터 및 필드 선언부- AssignmentCommandService.java
• 빌더 호출부.xAssignmentResponseDtos(…)→.attachments(…)필요 시 JSON 직렬화 이름이 변경되는지(
@JsonProperty) 확인하고, 클라이언트 영향 범위를 점검해 주세요.--- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentResponseDto.java @@ constructor 파라미터 - // 과제 첨부 데이터 - List<AssignmentAttachmentDto> xAssignmentResponseDtos + // 과제 첨부 데이터 + List<AssignmentAttachmentDto> attachments--- src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java @@ AssignmentResponseDto 빌더 호출부 - .xAssignmentResponseDtos(assignmentAttachmentDtos) + .attachments(assignmentAttachmentDtos)src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionDto.java (3)
3-3: 와일드카드 import 지양
import lombok.*;대신 사용되는 애너테이션만 명시적으로 임포트하면 가독성과 정적 분석 품질이 좋아집니다.-import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data;
7-10: DTO 일관성: record로의 통일 고려이번 PR에서 다수 DTO가
record + @Builder로 정의됩니다.SubmissionDto도record로 통일하면 불변성과 의도 전달이 개선됩니다.예시:
@Builder public record SubmissionDto( String title, LocalDateTime startDate, LocalDateTime endDate, String userName, boolean submitted, LocalDateTime submittedAt ) {}
19-21: boolean 필드isSubmitted명명 이슈 — 변경 시 모든 참조 갱신 필요Lombok/Jackson 상의 getter 충돌을 피하기 위해 필드명을
submitted로 변경하거나@JsonProperty("submitted")를 명시하는 리팩터링을 권장드립니다. 단, 다음 영역에 걸쳐 일괄적으로 참조를 업데이트해야 합니다:
- 도메인 엔티티
Submission
- 필드명, 생성자 파라미터,
submit(),cancel()내부 할당부- DTO 클래스
SubmissionDto(필드, 빌더 호출)SubmissionCheck(필드)- 서비스 레이어
SubmissionCommandService내 빌더 호출
.isSubmitted(s.getIsSubmitted())→.submitted(s.isSubmitted())- 저장소 쿼리
AssignmentRepositoryJPQL:s.isSubmitted→s.submitted- QueryDSL Q클래스
QSubmission필드명 재생성 (APT 재실행 필요)필드명 변경 예시:
// src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionDto.java - private boolean isSubmitted; + private boolean submitted;또는 리네임이 어렵다면, Jackson 직렬화명을 명시:
@JsonProperty("submitted") private boolean isSubmitted;위 변경 작업이 완료된 후 반드시 전체 빌드 및 JPQL/QueryDSL APT 결과를 검증해 주시기 바랍니다.
src/main/java/hello/cluebackend/domain/assignment/api/dto/request/CreateAssignmentDto.java (2)
3-16: 입력 값 검증 보강(@NotBlank/@Positive/@future…) 제안필수성만 체크하면 빈 문자열·음수·과거 시각이 통과될 수 있습니다. 도메인 무결성을 위해 제약을 강화하는 편이 안전합니다.
아래처럼 제약을 보강해 주세요(기존 @NotNull 유지/대체 기준 포함):
import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.*; import lombok.Builder; @Builder public record CreateAssignmentDto ( - @NotNull @JsonProperty("class_id") Long classId, - @NotNull String title, - @NotNull String content, - @NotNull @JsonProperty("start_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, - @NotNull @JsonProperty("end_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate + @NotNull @Positive @JsonProperty("class_id") Long classId, + @NotBlank String title, + @NotBlank String content, + @NotNull @FutureOrPresent @JsonProperty("start_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, + @NotNull @Future @JsonProperty("end_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate ){}
10-17: start/end 일관성 검증(교차 필드) 추가start_date ≤ end_date 보장은 DTO 레벨에서 잡아두면 서비스 단 예외 케이스가 크게 줄어듭니다. Record의 compact constructor로 방어 로직을 추가하는 것을 권장합니다.
아래처럼 record body를 열어 검증을 추가하세요:
-@Builder -public record CreateAssignmentDto ( +@Builder +public record CreateAssignmentDto ( @NotNull @Positive @JsonProperty("class_id") Long classId, @NotBlank String title, @NotBlank String content, @NotNull @FutureOrPresent @JsonProperty("start_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, @NotNull @Future @JsonProperty("end_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate -){} +){ + public CreateAssignmentDto { + if (endDate.isBefore(startDate)) { + throw new IllegalArgumentException("end_date must be after or equal to start_date"); + } + } +}src/main/java/hello/cluebackend/domain/assignment/persistence/AssignmentRepository.java (2)
14-18: JPQL 결과 중복 가능성(DISTINCT) 및 파라미터 표기 개선동일 과제에 대해 여러 제출 레코드가 생길 수 있다면(상태 이력 등) 중복 Assignment가 반환될 수 있습니다. DISTINCT를 추가하는 편이 안전합니다. 또한
=:userId는 동작하지만 가독성과 실수를 막기 위해= :userId로 정리하세요.- @Query("SELECT a FROM Submission s" + - " join s.assignment a" + - " where s.user.userId =:userId" + - " AND s.isSubmitted = false") + @Query("select distinct a from Submission s" + + " join s.assignment a" + + " where s.user.userId = :userId" + + " and s.isSubmitted = false") List<Assignment> getAllByUser(@Param("userId") Long userId);추가로, 메서드명은 Spring Data 컨벤션(동사: find…)에 맞춰
findUnsubmittedAssignmentsByUserId등으로 바꾸면 의도가 더 명확해집니다(호출부 영향 있어 선택 사항).
20-20: N+1 완화 및 조회 용도 분리 고려
findAllByClassRoom(ClassRoom)는 즉시로딩 설정에 따라 N+1이 발생할 수 있습니다. 빈번한 조회라면 필요한 연관 로딩에@EntityGraph(attributePaths=...)를 부착한 별도 메서드를 두는 것을 권장합니다.원하시면 EntityGraph 적용안까지 함께 제안드리겠습니다.
src/main/java/hello/cluebackend/domain/assignment/api/dto/request/ModifyAssignmentDto.java (1)
9-17: DTO 스타일 일관성(record vs class) 정리Create는 record(+@builder), Modify는 class(+@Setter)로 혼재되어 있습니다. 팀 규약에 따라 일관된 스타일을 선택해 주세요. Patch 성격상 null 허용/부분 수정이 필요하다면 class 유지가 합리적입니다만, 그렇다면 Create도 class로 통일하거나, Modify에만 명확히 null 허용 주석/스키마를 남겨 주세요.
원하시면 record 기반의 optional 필드 패턴 또는 class 기반의 불변+toBuilder 패턴으로 리팩터까지 도와드릴 수 있습니다.
src/main/java/hello/cluebackend/global/common/response/ApiResponse.java (1)
1-39: 주석만 존재하는 미완성 클래스 정리 필요현재 파일 전체가 주석 처리되어 있으며, 제네릭/리턴 타입도 불일치합니다(
error가List<Assignment>반환 등). 사용 계획이 없다면 제거, 사용할 예정이면 최소 구현을 완성해 주세요.아래처럼 최소 구현으로 정리하는 것을 제안합니다(외부 의존 제거, 단순/안전):
-//package hello.cluebackend.global.common.response; -// ... -//public class ApiResponse<T> { -// ... -//} +package hello.cluebackend.global.common.response; + +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ApiResponse<T> { + private int code; + private String message; + private T data; + + public static <T> ApiResponse<T> success(T data) { + return ApiResponse.<T>builder() + .code(200) + .message("OK") + .data(data) + .build(); + } + + public static <T> ApiResponse<T> error(int code, String message) { + return ApiResponse.<T>builder() + .code(code) + .message(message) + .build(); + } +}ApiResponse 사용처가 있는지 확인 부탁드립니다. 원하시면 검색 스크립트를 드리겠습니다.
src/main/java/hello/cluebackend/global/common/resolver/CurrentUserArgumentResolver.java (3)
31-33: 토큰 파싱 책임 배분 재고(Resolver vs Util)현재 리졸버가 Util과 책임이 다소 중복됩니다. Util은 “헤더→토큰”, 리졸버는 “토큰→userId”만 담당하도록 경계를 분리하면 테스트·교체 용이성이 올라갑니다.
원하시면 Util/Resolver 경계 재정의안과 단위 테스트 골격을 제안드리겠습니다.
18-19: 성능 메모: 중복 파싱 방지(선택)동일 요청 내 여러 파라미터에서 @currentuser를 사용할 경우, 토큰 파싱이 중복 실행됩니다. userId를 request attribute에 캐시하면 불필요한 파싱을 줄일 수 있습니다.
예: 파싱 후
request.setAttribute("CURRENT_USER_ID", userId)저장, 존재 시 재사용.
21-24: CurrentUserArgumentResolver에서 primitivelong지원 추가 권장현재
supportsParameter메서드는@CurrentUser애노테이션이 붙은 파라미터 중에서 오직Long.class타입만 인식합니다. primitivelong타입에도 동일하게 적용되도록 처리하지 않으면, 다음과 같이 선언된 컨트롤러 메서드는 지원되지 않습니다:public ResponseEntity<?> getProfile(@CurrentUser long userId) { … }레포지토리 전반을 확인한 결과
@CurrentUser long사용 사례는 없었으나, 향후 예기치 않은 사용 방지를 위해 primitivelong지원을 추가하는 것을 권장드립니다.
대상 위치
src/main/java/hello/cluebackend/global/common/resolver/CurrentUserArgumentResolver.java (21–24라인)수정 제안
@Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(CurrentUser.class) - && parameter.getParameterType().equals(Long.class); + return parameter.hasParameterAnnotation(CurrentUser.class) + && (parameter.getParameterType().equals(Long.class) + || parameter.getParameterType().equals(long.class)); }src/main/java/hello/cluebackend/domain/assignment/api/dto/response/GetAllAssignmentDto.java (2)
9-22: DTO 스타일 일관성: 기존 record 기반과 달라 유지보수 혼선 가능동일 컨텍스트에서
GetAssignmentResponseDto는 record인데 이 DTO는 Lombok 클래스(@DaTa, @builder)입니다. 한 도메인 내 응답 DTO의 선언 스타일을 통일하는 것을 권장합니다.두 가지 방향 중 택1:
- record로 전환(불변, 보일러플레이트 축소):
-@Data -@Builder -public class GetAllAssignmentDto { - private Long assignmentId; // 과제 아이디 - private String title; // 과제 제목 - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime startDate; // 과제 시작일 - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime endDate; // 마감일 - - public GetAllAssignmentDto(Long assignmentId, String title, LocalDateTime startDate, LocalDateTime endDate){ - this.assignmentId = assignmentId; - this.title = title; - this.startDate = startDate; - this.endDate = endDate; - } -} +public record GetAllAssignmentDto( + Long assignmentId, + String title, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate +) {}
- 혹은 Lombok 유지 시 가변성 축소(@Getter + @AllArgsConstructor; @DaTa 제거):
-@Data -@Builder +@Getter +@Builder +@AllArgsConstructor public class GetAllAssignmentDto { - private Long assignmentId; - private String title; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime startDate; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime endDate; - - public GetAllAssignmentDto(Long assignmentId, String title, LocalDateTime startDate, LocalDateTime endDate){ - this.assignmentId = assignmentId; - this.title = title; - this.startDate = startDate; - this.endDate = endDate; - } + private final Long assignmentId; + private final String title; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private final LocalDateTime startDate; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private final LocalDateTime endDate; }
14-15: 날짜 직렬화 타임존 명시 누락 가능성
LocalDateTime에 타임존을 포함하지 않는 포맷을 사용 중이라 서버/클라이언트 TZ 차이로 혼선이 생길 수 있습니다. ISO-8601(오프셋 포함) 또는 명시적 timezone 지정 고려 바랍니다.예:
-@JsonFormat(pattern = "yyyy-MM-dd HH:mm") +@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") private LocalDateTime startDate; -@JsonFormat(pattern = "yyyy-MM-dd HH:mm") +@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX") private LocalDateTime endDate;또는 기존 패턴 유지 시:
-@JsonFormat(pattern = "yyyy-MM-dd HH:mm") +@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")src/main/java/hello/cluebackend/domain/classroomuser/application/ClassroomUserService.java (2)
21-28: 읽기 전용 트랜잭션 및 N+1 완화, 람다 간소화현재 LAZY 연관의
getUser()접근 시 Open-in-View가 꺼져 있으면LazyInitializationException이 발생할 수 있습니다. 조회 메서드에@Transactional(readOnly = true)추가 권장하며, 메서드 레퍼런스로 가독성을 높일 수 있습니다. 필요 시 Repository 레벨에서 fetch join으로 N+1 완화도 고려하세요.+import org.springframework.transaction.annotation.Transactional; ... - public List<UserEntity> findAllClassroomUser(ClassRoom classRoom){ + @Transactional(readOnly = true) + public List<UserEntity> findAllClassroomUser(ClassRoom classRoom){ List<ClassRoomUser> classRoomUsers = classRoomUserRepository.findAllByClassRoom(classRoom); - List<UserEntity> users = classRoomUsers.stream() - .map(cru -> cru.getUser()) - .toList(); + List<UserEntity> users = classRoomUsers.stream() + .map(ClassRoomUser::getUser) + .toList(); return users; }또한 네이밍을
findAllUsersInClassroom등으로 바꾸면 의도가 더 명확합니다. 변경 시 호출부 영향 검증 필요.
30-36: 설계 계층 및 성능 개선 제안: DTO 의존 제거현재
ClassRoomService.findById(...)는ClassRoomDto를,UserService.findById(...)는UserDto를 반환하며 각 DTO의toEntity()를 호출하고 있습니다. DTO에toEntity()메서드가 존재하여 컴파일 오류는 발생하지 않지만, 애플리케이션 서비스 계층이 프레젠테이션 DTO에 의존하는 계층 누수와 불필요한 객체 변환이 발생합니다.
대상 위치
src/main/java/hello/cluebackend/domain/classroomuser/application/ClassroomUserService.java(32–36행)제안 1: ID 기반
exists쿼리로 직접 확인- public boolean isUserInClassroom(Long classRoomId, Long userId) { - ClassRoom classRoom = classRoomService.findById(classRoomId).toEntity(); - UserEntity user = userService.findById(userId).toEntity(); - return classRoomUserRepository.existsByClassRoomAndUser(classRoom, user); - } + public boolean isUserInClassroom(Long classRoomId, Long userId) { + return classRoomUserRepository + .existsByClassRoom_ClassRoomIdAndUser_UserId(classRoomId, userId); + }
- 추가해야 할 리포지토리 메서드:
boolean existsByClassRoom_ClassRoomIdAndUser_UserId(Long classRoomId, Long userId);제안 2: 엔티티 반환 전용 서비스 분리
findByIdEntity(Long id)같은 메서드를 별도로 두어, DTO → 엔티티 변환이 아닌 애초에 엔티티를 가져오도록 분리할 수 있습니다.위 변경을 통해 프레젠테이션 계층과 애플리케이션 계층 간의 결합도를 낮추고, DB 조회 효율성 및 코드 가독성을 개선할 수 있습니다.
src/main/java/hello/cluebackend/domain/submission/domain/Submission.java (4)
14-16: 엔티티 전체 @Setter로 불변성 약화엔티티 레벨의
@Setter는 도메인 규칙 우회를 초래할 수 있습니다. 상태 변경 메서드를 통해서만 변경되도록 필요한 필드에만 setter를 제한하거나 제거를 권장합니다.-@Builder @Setter +@Builder public class Submission extends BaseEntity{필요한 필드에만 개별
@Setter부여 또는 전용 도메인 메서드 유지.
47-51: 취소 시각 처리 의미 재검토(취소에 현재시각 기록 vs 제출시각 초기화)
cancel()에서submittedAt을 현재 시각으로 덮어씁니다. "제출 취소"라면submittedAt을null로 초기화하거나 별도canceledAt필드를 두는 편이 의미적으로 더 명확합니다.public void cancel() { this.isSubmitted = false; - this.submittedAt = LocalDateTime.now(); + this.submittedAt = null; }의도 확인 후 일관되게 사용해 주세요.
42-45: 연관관계 변경 시 이전 연관 해제 누락 가능
changeAssignment가 기존assignment와의 연관을 제거하지 않아 재할당 시 양쪽 컬렉션 불일치/중복 위험이 있습니다.public void changeAssignment(Assignment assignment) { - this.assignment = assignment; - assignment.getSubmissions().add(this); + if (this.assignment != null) { + this.assignment.getSubmissions().remove(this); + } + this.assignment = assignment; + if (assignment != null && !assignment.getSubmissions().contains(this)) { + assignment.getSubmissions().add(this); + } }
59-62: 불필요한 boolean getter 중복Lombok
@Getter가 boolean 필드에 대해isSubmitted()를 생성합니다. 수동getIsSubmitted()는 중복이자 프레임워크 바인딩 혼선을 유발할 수 있습니다.- // getter - public boolean getIsSubmitted(){ - return this.isSubmitted; - } + // Lombok이 isSubmitted() 생성src/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java (3)
6-6: 미사용 import 정리
org.springframework.core.io.Resource는 사용되지 않습니다. 제거하여 경고를 없애 주세요.-import org.springframework.core.io.Resource;
15-17: 필드 네이밍 컨벤션 위반(Camel Case)
SubmissionAttachmentId의 필드명이 대문자로 시작합니다. 자바 컨벤션에 맞게 camelCase로 변경 권장합니다. DB 컬럼명은 이미 지정되어 영향이 제한적입니다.- private Long SubmissionAttachmentId; + private Long submissionAttachmentId;변경 시 Lombok/스프링 데이터 접근자 영향 점검 필요.
8-14: 감사/공통 속성 일관성: BaseEntity 상속 고려
AssignmentAttachment는BaseEntity를 상속하지만SubmissionAttachment는 상속하지 않습니다. 생성/수정 시각 등 공통 메타데이터가 필요하다면 일관성을 위해 상속을 권장합니다.-public class SubmissionAttachment { +public class SubmissionAttachment extends BaseEntity {src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java (3)
65-71: 오타: storedFiledName 변수명 수정 권장.
storedFiledName→storedFileName오타입니다. 가독성과 일관성을 위해 정정하세요.- String storedFiledName = fileService.storeFile(file); + String storedFileName = fileService.storeFile(file); @@ - .value(storedFiledName) + .value(storedFileName)
60-77: 파일 업로드 입력 검증 부족(빈 파일/사이즈/콘텐츠 타입).빈 파일, 과도한 파일 크기, 비허용 MIME 타입에 대한 선제적 검증이 없습니다. 최소한의 입력 검증을 추가해 주세요.
public SubmissionAttachment fileUpload(Long submissionId, MultipartFile file) { Submission submission = submissionCommandService.findByIdOrThrow(submissionId); + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("빈 파일은 업로드할 수 없습니다."); + } + if (file.getSize() > 50L * 1024 * 1024) { // 50MB 예시 + throw new IllegalArgumentException("파일이 허용 용량을 초과했습니다."); + } + // 필요 시 MIME 화이트리스트 확인 + // Set.of("image/png","image/jpeg","application/pdf") 등 + String storedFiledName = fileService.storeFile(file);참고: 위 검증 정책은 실제 요구사항에 맞게 조정해 주세요.
1-21: (참고) AccessDeniedException import 필요.위 인가 로직을 도입하면
org.springframework.security.access.AccessDeniedExceptionimport가 필요합니다.추가 import:
import org.springframework.security.access.AccessDeniedException;src/main/java/hello/cluebackend/domain/file/service/FileService.java (3)
47-54: 예외 삼키기: 삭제 실패 시 원인 정보 손실.모든 예외를 잡아 메시지 없이
RuntimeException("파일 삭제에 실패했습니다.")로 치환하면 원인 분석이 어렵습니다. 원인 포함 및 S3 예외 구분을 권장합니다.- public void deleteFile(String storedFileName) { - try{ - amazonS3Client.deleteObject(bucket, storedFileName); - }catch(Exception e){ - throw new RuntimeException("파일 삭제에 실패했습니다."); - } - } + public void deleteFile(String storedFileName) { + try { + amazonS3Client.deleteObject(bucket, storedFileName); + } catch (Exception e) { + throw new RuntimeException("파일 삭제에 실패했습니다: " + e.getMessage(), e); + } + }
56-63: 메서드 시그니처 정리 제안: 불필요한 throws 선언.
downloadFile은 내부에서 모든 예외를 잡아S3FileNotFoundException으로 변환하므로throws IOException이 불필요합니다. 제거 시 호출부(다운로드 컨트롤러/서비스)도 함께 정리하세요.- public Resource downloadFile(String filePath) throws IOException { + public Resource downloadFile(String filePath) {호출부의
throws IOException제거도 병행 필요.
39-44: 퍼블릭 접근 주석과 실제 ACL 설정 불일치(정보).주석에 '공개 권한 설정'이라고 되어 있으나
PutObjectRequest에 ACL 설정이 없습니다. 내부 프록시 다운로드 방식이면 현재 구현으로 충분하지만, 퍼블릭 접근이 요구된다면withCannedAcl(CannedAccessControlList.PublicRead)등을 명시하세요.src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java (3)
58-64: findAllById: 불필요한 인자와 N+1 패턴 가능성
- userId 파라미터가 사용되지 않습니다.
- 각 Assignment에 대해 다시 findById(=DB 조회 + 첨부 조회) 호출로 N+1이 발생할 수 있습니다.
메서드 시그니처를 의도에 맞게 바꾸고(예: findAllByClassId), 현재 가져온 assignments 컬렉션으로 바로 DTO를 구성하도록 리팩터링을 제안합니다.
다음과 같이 최소 변경으로 개선할 수 있습니다.
- public List<AssignmentResponseDto> findAllById(Long userId, Long classId) { + public List<AssignmentResponseDto> findAllByClassId(Long classId) { ClassRoom classRoom = classRoomService.findById(classId).toEntity(); List<Assignment> assignments = assignmentRepository.findAllByClassRoom(classRoom); - return assignments.stream() - .map(a -> findById(a.getAssignmentId())) - .toList(); + return assignments.stream() + .map(this::toAssignmentResponseDto) // 재조회 없이 DTO 구성 + .toList(); }추가 메서드(파일 내 적절한 위치에 정의):
// 파일 내 추가 (private helper) private AssignmentResponseDto toAssignmentResponseDto(Assignment a) { List<AssignmentAttachment> assignmentAttachments = assignmentAttachmentRepository.findAllByAssignment(a); List<AssignmentAttachmentDto> dtos = assignmentAttachments.stream() .map(aa -> AssignmentAttachmentDto.builder() .type(aa.getType()) .value(aa.getValue()) .originalFileName(aa.getOriginalFileName()) .contentType(aa.getContentType()) .size(aa.getSize()) .build()) .toList(); return AssignmentResponseDto.builder() .assignmentId(a.getAssignmentId()) .title(a.getTitle()) .content(a.getContent()) .startDate(a.getStartDate()) .endDate(a.getEndDate()) .userName(a.getUser().getUsername()) .xAssignmentResponseDtos(dtos) // 실제 필드명에 맞게 조정 .build(); }컨트롤러 호출부도 findAllByClassId로 정리해 주세요.
72-75: 메서드명 오탈자: findAssignmentAttachmentByIdOrderThrow → OrThrow‘Order’는 오타로 보입니다. 다른 메서드(findByIdOrThrow)와 일관되게 ‘OrThrow’를 사용해 주세요.
- public AssignmentAttachment findAssignmentAttachmentByIdOrderThrow(Long attachmentId){ + public AssignmentAttachment findAssignmentAttachmentByIdOrThrow(Long attachmentId){ return assignmentAttachmentRepository.findById(attachmentId) .orElseThrow(() -> new EntityNotFoundException("해당 첨부 파일을 찾을수 없습니다.")); }이 변경에 따른 호출부(예: AssignmentCommandController)도 함께 수정해야 합니다.
46-55: AssignmentResponseDto 빌더 필드명 및 DTO import 일관성 검토 필요
현재
AssignmentResponseDto에List<AssignmentAttachmentDto> xAssignmentResponseDtos필드가 정의되어 있고,AssignmentCommandService에서도.xAssignmentResponseDtos(…)로 호출되고 있어 컴파일 오류는 없습니다.다만
x접두사는 직관적이지 않아, 필드명 및 빌더 메서드를attachments또는attachmentDtos등으로 바꾸는 것을 권장합니다. 변경 시 아래 위치를 함께 수정해야 합니다:
src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentResponseDto.java
기존:제안:List<AssignmentAttachmentDto> xAssignmentResponseDtos;List<AssignmentAttachmentDto> attachmentDtos;src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java
기존:제안:.xAssignmentResponseDtos(assignmentAttachmentDtos).attachmentDtos(assignmentAttachmentDtos)추가로,
AssignmentAttachmentDto의 import 경로가 파일마다 다릅니다:
AssignmentQueryController.java·AssignmentQueryService.java→...api.dto.request.AssignmentAttachmentDtoAssignmentCommandService.java·AssignmentResponseDto.java→...api.dto.response.AssignmentAttachmentDto→ 요청(request)과 응답(response)용 DTO 패키지를 명확히 구분하거나 클래스명을 달리해 혼동을 줄이시길 권장드립니다.
src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java (1)
41-45: 날짜 직렬화 패턴/타임존 정책 점검 권장@jsonformat(pattern = "yyyy-MM-dd HH:mm")은 타임존 정보가 없어 서버/클라이언트 TZ 불일치 시 오프셋 문제가 생길 수 있습니다. 전역 ObjectMapper 설정(ISO-8601 + UTC) 또는 pattern에 timezone을 명시하는 방향을 검토해 주세요.
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java (2)
60-73: 파일 업로드: 빈 배열/널 처리 및 예외 선언 정리
- files가 null 또는 비어있을 때의 처리 로직이 없습니다.
- 서비스 계층에서 IOException을 런타임으로 전환한다면 컨트롤러 시그니처의 throws IOException 제거가 가능합니다.
- public ResponseEntity<?> uploadAttachments( + public ResponseEntity<?> uploadAttachments( @@ - ) throws IOException { + ) { - for (MultipartFile file : files) { + if (files == null || files.length == 0) { + return ResponseEntity.badRequest().body("업로드할 파일이 없습니다."); + } + for (MultipartFile file : files) { assignmentQueryService.uploadFileAttachment(assignmentId, file); }
86-94: 삭제 응답 메시지 오타/맥락 불일치첨부 삭제인데 “과제를 성공적으로 삭제했습니다.”라고 응답합니다. 메시지를 정확히 바꿔 주세요.
- return ResponseEntity.ok("과제를 성공적으로 삭제했습니다."); + return ResponseEntity.ok("첨부 파일을 성공적으로 삭제했습니다.");src/main/java/hello/cluebackend/domain/submission/application/SubmissionCommandService.java (3)
49-64: findAllByAssignmentId: userId 파라미터 미사용userId가 시그니처에 있으나 사용되지 않습니다. 의도적으로 “전체 제출 현황”을 반환한다면 파라미터 제거를, 특정 사용자 기준이라면 필터 조건을 추가하세요.
예: 특정 사용자 기준으로 제한하려면
- public List<SubmissionDto> findAllByAssignmentId(Long userId, Long assignmentId) { + public List<SubmissionDto> findAllByAssignmentId(Long userId, Long assignmentId) { @@ - return submissions.stream() + return submissions.stream() + .filter(s -> s.getUser().getUserId().equals(userId)) .map(s -> SubmissionDto.builder()또는 파라미터 삭제/오버로드 제공을 검토하세요.
87-90: 메서드 중복/명명 불일치: findAssignmentAttachmentByIdOrThrow vs findsubmissionAttachmentByIdOrThrow두 메서드가 동일한 동작을 중복 수행하고, 하나는 타이포(s 소문자)입니다. 하나로 통합하고 일관된 이름을 사용하세요.
- public SubmissionAttachment findsubmissionAttachmentByIdOrThrow(Long submissionAttachmentId) { - return submissionAttachmentRepository.findById(submissionAttachmentId) - .orElseThrow(() -> new EntityNotFoundException("해당 과제 제출 첨부파일을 찾을수 없습니다.")); - } + // 위 findAssignmentAttachmentByIdOrThrow만 유지 또는 이름을 findSubmissionAttachmentByIdOrThrow로 통일또한 예외 메시지도 “첨부파일” 등으로 일관되게 맞춰 주세요.
92-104: 메서드 네이밍: findAllAssignment → findAllAttachments(…)반환 내용이 첨부파일 목록이므로 의미가 드러나도록 메서드명을 변경해 주세요.
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java (3)
43-50: 권한 확인 전에 불필요한 DTO 조회 및 DB 2회 접근현재 흐름: DTO 조회(findById) → 엔티티 재조회(findByIdOrThrow) → 권한 체크 → DTO 반환. 권한이 없더라도 DTO를 먼저 생성합니다. 권한 검증을 먼저 수행하고 필요 시 DTO를 조회하도록 순서를 바꾸세요.
- AssignmentResponseDto result = assignmentCommandService.findById(assignmentId); - Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); + Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); Long classroomId = assignment.getClassRoom().getClassRoomId(); if (!classroomUserService.isUserInClassroom(classroomId, userId)) { throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); } + AssignmentResponseDto result = assignmentCommandService.findById(assignmentId); return ResponseEntity.ok(result);추후에는 서비스에서 한 번의 조회로 권한 판단 + DTO 생성까지 처리하는 오버로드를 제공하면 더 좋습니다.
69-74: 리소스 경로 혼재: 과제 컨트롤러에서 제출 첨부파일을 조회
GET /api/assignments/{submissionId}/attachment는 자원의 소속이 혼동됩니다./api/submissions/{submissionId}/attachments로 일관되게 분리하는 것을 권장합니다. 또한 접근 제어(해당 제출의 과제 교실 소속 검증)도 함께 수행하세요.
24-26: 사용되지 않는 import 정리
import java.io.File;는 사용되지 않습니다. 제거해 주세요.
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
Show resolved
Hide resolved
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
Show resolved
Hide resolved
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java
Show resolved
Hide resolved
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java
Show resolved
Hide resolved
| SubmissionAttachment result = SubmissionAttachment.builder() | ||
| .submission(submission) | ||
| .type(fileType.file) | ||
| .value(storedFiledName) | ||
| .originalFileName(file.getOriginalFilename()) | ||
| .contentType(file.getContentType()) | ||
| .size(file.getSize()) | ||
| .build(); |
There was a problem hiding this comment.
파일 타입 enum 상수 사용이 불일치하여 컴파일 오류 가능성 (fileType.FILE/URL vs fileType.file/url).
동일 PR의 Assignment 쪽에서는 fileType.FILE, fileType.URL을 사용하고 있는데, 이 파일은 fileType.file, fileType.url을 사용합니다. Enum 상수는 대소문자까지 일치해야 하므로 현재 상태로는 컴파일 실패합니다.
아래와 같이 수정해 주세요.
@@
- .type(fileType.file)
+ .type(fileType.FILE)
@@
- .type(fileType.url)
+ .type(fileType.URL)Also applies to: 84-88
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java
around lines 67-74 (and also update the similar occurrence at lines 84-88), the
enum constants are referenced with wrong casing (fileType.file / fileType.url)
which will cause compile errors; update those references to the exact enum
constant names used elsewhere (e.g. fileType.FILE and fileType.URL) so casing
matches the enum declaration, and verify both places are corrected to the same
constants.
| public enum fileType { | ||
| file, url | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
중복 enum 정의 통합 필요 (assignment의 fileType과 값 불일치)
동일 개념의 enum이 submission/assignment 패키지에 각각 존재하며, 상수 케이스도 서로 다릅니다. 도메인 간 상호 참조/매핑, 직렬화(JSON), DB 문자열 저장 시 불일치로 인한 버그 가능성이 큽니다.
- 제안: 공용 패키지(예:
hello.cluebackend.global.common.enums.FileType)로 단일화하고, 두 도메인에서 동일 enum을 참조하도록 리팩터링하세요. - 이미 DB에 문자열이 저장되어 있다면 마이그레이션(예:
file/url→FILE/URL, 혹은LINK→URL) 계획이 필요합니다.
필요 시 enum 내부에 역호환 파서를 두어 과거 값도 수용할 수 있습니다(@JsonCreator로 "link" → URL 매핑 등).
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/domain/submission/domain/fileType.java lines
3-5: the project has duplicate enums for the same concept with differing
constant names which risks serialization/DB mismatch; refactor by creating a
single shared enum (e.g. hello.cluebackend.global.common.enums.FileType) with
unified constants (FILE, URL) and move all usages in submission and assignment
packages to reference this common enum, update imports/usages, and remove the
duplicate enum; add a @JsonCreator static factory inside the new enum to accept
legacy string values (e.g. "file", "url", "link") and map them to the new
constants for backward compatibility, and if DB already contains legacy strings,
prepare a migration plan to convert stored values to the new standardized names.
💡 Verification agent
🧩 Analysis chain
레거시 값/참조 잔존 여부 점검 스크립트
두 개념 중복과 문자열 값 변화로 인한 런타임 이슈를 예방하기 위해 다음 점검을 권장합니다.
🏁 Script executed:
#!/bin/bash
# 1) enum 중복/참조 현황
rg -nP '\bfileType\b|\bFileType\b|\bSubmitType\b' -C2
# 2) JPA 문자열 매핑 사용 지점
rg -nP '@Enumerated\s*\(\s*EnumType\.STRING\s*\)'
# 3) 직렬화 영향(JSON): 컨트롤러/DTO에서의 사용
rg -nP --type=java -C2 '\b(fileType|FileType|SubmitType)\b' src/main/java | rg -nP -C2 'Dto|Controller|Request|Response'
# 4) (선택) 쿼리/원시 SQL 내 하드코딩된 값
rg -nP -C2 '\b(FILE|URL|file|url|LINK|IMAGE)\b' -g '!**/node_modules/**'Length of output: 47296
Submit/Assignment fileType enum 통합 및 매핑 일관성 확보 필요
두 개의 fileType enum(submission.domain.fileType, assignment.domain.fileType)이 서로 다른 케이스와 값(file, url vs FILE, URL)으로 정의되어 있어,
JPA 문자열 매핑(@Enumerated(EnumType.STRING)) 및 JSON 직렬화 시 불일치에 따른 런타임/호환성 이슈가 발생할 수 있습니다.
해당 enum이 사용되는 위치
- Submission 도메인
src/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java(private fileType type;)src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionAttachmentDto.java(fileType type)src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java(.type(fileType.file),.type(fileType.url))src/main/java/hello/cluebackend/domain/submission/api/SubmissionQueryController.java(fileUpload API)- Querydsl 생성물:
QSubmissionAttachment.java(EnumPath<fileType> type)
- Assignment 도메인
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java(private fileType type;)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java(fileType type)src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java(.type(fileType.FILE),.type(fileType.URL))src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java(uploadAttachments API)- Querydsl 생성물:
QAssignmentAttachment.java(EnumPath<fileType> type)
권장 조치
- 두 도메인의
fileTypeenum을 하나로 통합하거나, 용도에 따라SubmissionFileType,AssignmentFileType등으로 명확히 분리 및 리팩터링 - enum 값(String) 표기에 대한 일관성 확보(소문자⇔대문자 통일 또는 Jackson 커스텀 Serializer 적용)
- 매핑 테스트 및 JSON 직렬화/역직렬화 테스트 추가
- enum명 변경 시 Querydsl 코드 재생성 및 관련 서비스·컨트롤러·DTO 일괄 수정
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/domain/submission/domain/fileType.java lines
3-5: the enum here (fileType with values file,url) conflicts in name/case with
the assignment domain enum (FILE,URL); replace this by either (1) consolidating
into a single shared enum (e.g.,
src/main/java/hello/cluebackend/domain/common/FileType with consistent constant
names like FILE, URL) or (2) renaming to a purposeful domain-specific enum
(SubmissionFileType) with matching values and case to the assignment side, then
update all usages (entities, DTOs, services, controllers) to the new enum type,
adjust @Enumerated(EnumType.STRING) and add Jackson annotations if you need
custom JSON names, regenerate Querydsl sources, and add mapping/serialization
tests to validate JPA string mapping and JSON (de)serialization consistency.
🛠️ Refactor suggestion
Enum 네이밍/케이스 불일치: 자바 컨벤션과 JPA 문자열 매핑 위험
- 타입명은 PascalCase(
FileType), 상수는 UPPER_SNAKE_CASE(FILE,URL)가 권장입니다. - 현재 상수 값이 소문자(
file,url)라@Enumerated(EnumType.STRING)사용 시 DB에 소문자 값이 저장/매핑되어 동일 개념의 다른 enum과 불일치가 발생할 수 있습니다.
다음과 같이 정규화 제안드립니다. 파일명도 FileType.java로 변경 필요합니다.
-package hello.cluebackend.domain.submission.domain;
+package hello.cluebackend.domain.submission.domain;
-public enum fileType {
- file, url
-}
+public enum FileType {
+ FILE, URL
+}📝 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.
| public enum fileType { | |
| file, url | |
| } | |
| package hello.cluebackend.domain.submission.domain; | |
| public enum FileType { | |
| FILE, URL | |
| } |
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/domain/submission/domain/fileType.java around
lines 3 to 5, the enum and its constants do not follow Java naming conventions
and risk incorrect DB string mappings; rename the file to FileType.java, change
the enum name to FileType, and rename constants to FILE and URL
(UPPER_SNAKE_CASE) so that @Enumerated(EnumType.STRING) stores consistent,
conventional values; update all imports/usages across the codebase to the new
name and adjust any DB fixtures/migrations that reference the old lowercase
strings.
There was a problem hiding this comment.
@Hgyeol 네, 맞습니다! Java 네이밍 컨벤션을 따라 대문자로 변경하는 것이 필요합니다.
다음과 같이 refactoring을 진행하시면 됩니다:
1. 파일명 및 enum 이름 변경
// 현재: src/main/java/hello/cluebackend/domain/submission/domain/fileType.java
// 변경: src/main/java/hello/cluebackend/domain/submission/domain/FileType.java
public enum FileType {
FILE, URL
}2. 모든 참조 업데이트
다음 파일들에서 fileType을 FileType으로, file/url을 FILE/URL로 변경:
SubmissionAttachment.javaSubmissionAttachmentDto.javaSubmissionQueryService.java- QueryDSL 생성 파일들 (재생성 필요)
3. Assignment 도메인과의 일관성
Assignment 도메인의 fileType enum도 동일하게 대문자 상수로 통일하거나, 공통 enum으로 통합을 고려해보세요.
4. DB 마이그레이션 고려사항
기존 DB에 소문자 값('file', 'url')이 저장되어 있다면, 대문자 값으로 변환하는 마이그레이션이 필요할 수 있습니다.
이런 변경으로 Java 표준을 준수하고 JPA 문자열 매핑의 일관성을 확보할 수 있습니다.
✏️ Learnings added
Learnt from: Hgyeol
PR: EscapeFrame/clueBackend#48
File: src/main/java/hello/cluebackend/domain/submission/domain/fileType.java:3-5
Timestamp: 2025-08-26T14:09:13.438Z
Learning: 사용자 Hgyeol은 Java 네이밍 컨벤션을 준수하는 것을 선호하며, enum 상수는 UPPER_SNAKE_CASE로 작성하는 것을 지지한다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| public interface SubmissionRepository extends JpaRepository<Submission, Long> { | ||
| List<Submission> findAllByAssignment(Assignment assignment); | ||
|
|
||
| Submission findByAssignment(Assignment assignment); | ||
| } |
There was a problem hiding this comment.
다대일 카디널리티에서 단건 조회 메서드 findByAssignment는 런타임 예외 유발 소지 있음
동일 과제에 여러 제출(학생별 다건)이 가능하다면, Submission findByAssignment(Assignment assignment)는 결과가 2건 이상일 때 IncorrectResultSizeDataAccessException이 발생할 수 있습니다. 과제+제출자(유저)로 유일성이 결정되는 도메인이라면 키를 명확히 하거나, 최신 1건만 필요하다면 정렬 기준을 명시하세요.
적용 예시(유저 단건 조회) — 도메인 타입명은 실제 엔티티에 맞게 수정 필요:
- Submission findByAssignment(Assignment assignment);
+ Optional<Submission> findByAssignmentAndUser(Assignment assignment, UserEntity user);최신 1건만 필요하다면(예: 제출일시 기준):
- Submission findByAssignment(Assignment assignment);
+ Optional<Submission> findFirstByAssignmentOrderByCreatedAtDesc(Assignment assignment);참고: 위 변경은 서비스/테스트 호출부 전반 수정이 필요합니다.
📝 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.
| public interface SubmissionRepository extends JpaRepository<Submission, Long> { | |
| List<Submission> findAllByAssignment(Assignment assignment); | |
| Submission findByAssignment(Assignment assignment); | |
| } | |
| public interface SubmissionRepository extends JpaRepository<Submission, Long> { | |
| List<Submission> findAllByAssignment(Assignment assignment); | |
| Optional<Submission> findByAssignmentAndUser(Assignment assignment, UserEntity user); | |
| } |
| @Override | ||
| public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { | ||
| converters.add(octetStreamReadMsgConverter); | ||
| } | ||
|
|
There was a problem hiding this comment.
configureMessageConverters 오버라이드로 기본 컨버터가 사라질 수 있음
configureMessageConverters를 오버라이드하면 기본 컨버터 등록이 우회되어 JSON 변환 등 기본 동작이 깨질 수 있습니다. 단순 추가 목적이라면 extendMessageConverters를 사용하세요.
- @Override
- public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
- converters.add(octetStreamReadMsgConverter);
- }
+ @Override
+ public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
+ // 필요 시 우선순위 조정을 위해 앞쪽 삽입
+ converters.add(0, octetStreamReadMsgConverter);
+ }📝 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.
| @Override | |
| public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { | |
| converters.add(octetStreamReadMsgConverter); | |
| } | |
| @Override | |
| public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { | |
| // 필요 시 우선순위 조정을 위해 앞쪽 삽입 | |
| converters.add(0, octetStreamReadMsgConverter); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/WebConfig.java around lines 23
to 27, the override of configureMessageConverters replaces the framework's
default converters (breaking JSON and other defaults); change this to override
extendMessageConverters and add octetStreamReadMsgConverter to the provided
list, or remove configureMessageConverters and implement public void
extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(octetStreamReadMsgConverter); } so default converters are
preserved.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java (3)
15-15: PATCH 메서드 허용 누락 가능성.REST 업데이트에 PATCH를 사용한다면 함께 허용하세요.
- .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
13-13: 주석 처리된 오리진 라인 정리.죽은 코드는 혼란을 유발합니다. 필요하면 프로퍼티로 관리하세요.
-// .allowedOriginPatterns("http://10.150.149.87:7789") // 프론트 IP + 포트
18-18: Preflight 캐싱 maxAge 설정으로 성능 최적화.빈번한 OPTIONS 프리플라이트를 줄이기 위해 캐시 수명을 명시하세요.
- .allowCredentials(true); // 쿠키, 인증 정보 허용 + .allowCredentials(true) // 쿠키, 인증 정보 허용 + .maxAge(3600); // 프리플라이트 캐시(초)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java(1 hunks)src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java(2 hunks)src/main/java/hello/cluebackend/domain/user/presentation/TestController.java(1 hunks)src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java
- src/main/java/hello/cluebackend/domain/user/presentation/TestController.java
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java
- src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java
- src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java (1)
src/main/java/hello/cluebackend/global/config/SecurityConfig.java (1)
getCorsConfiguration(74-84)
🔇 Additional comments (1)
src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java (1)
12-12: 전 구간 매핑("/")이 의도된 범위인지 확인 부탁드립니다.**관리/액추에이터 등도 포함될 수 있습니다. 필요 시 API 프리픽스로 범위를 축소하세요(예: "/api/**").
| @Override | ||
| public void addCorsMappings(CorsRegistry registry) { | ||
| registry.addMapping("/**") // 모든 API 경로 허용 | ||
| // .allowedOriginPatterns("http://10.150.149.87:7789") // 프론트 IP + 포트 | ||
| .allowedOriginPatterns("*") | ||
| .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 | ||
| .allowedHeaders("*") // 모든 요청 헤더 허용 | ||
| .exposedHeaders("Authorization", "Set-Cookie") // 클라이언트가 접근 가능한 헤더 | ||
| .allowCredentials(true); // 쿠키, 인증 정보 허용 | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
CORS 설정이 SecurityConfig와 WebMvcConfigurer에 중복·불일치합니다 — 단일 소스로 통일하세요.
SecurityConfig에서 별도의 CorsConfigurationSource를 등록하고 있고(오리진·메서드·헤더 설정 상이), 본 클래스도 전역 CORS를 설정합니다. Spring Security의 CORS가 우선 적용되므로 엔드포인트별로 상이한 동작/디버깅 난이도 상승이 발생합니다. 하나만 유지하거나, 공용 CorsConfigurationSource @bean을 만들어 Security/MVC가 동일 설정을 참조하도록 정리하세요.
단기 해법(이 파일에서 CORS 제거 — SecurityConfig만 사용):
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping("/**") // 모든 API 경로 허용
-// .allowedOriginPatterns("http://10.150.149.87:7789") // 프론트 IP + 포트
- .allowedOriginPatterns("*")
- .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
- .allowedHeaders("*") // 모든 요청 헤더 허용
- .exposedHeaders("Authorization", "Set-Cookie") // 클라이언트가 접근 가능한 헤더
- .allowCredentials(true); // 쿠키, 인증 정보 허용
- }
+ // CORS는 SecurityConfig의 CorsConfigurationSource로 단일 관리레포 내 CORS 설정 분포를 점검하려면:
🏁 Script executed:
#!/bin/bash
rg -n -C2 -g '!**/build/**' -g '!**/out/**' -S '(addCorsMappings|CorsConfigurationSource|\.cors\(|corsCustomizer|allowedOriginPatterns\()' --type=javaLength of output: 2217
🏁 Script executed:
#!/bin/bash
# SecurityConfig 내 CORS 설정 확인
sed -n '60,90p' src/main/java/hello/cluebackend/global/config/SecurityConfig.javaLength of output: 1639
CORS 설정 중복 제거 및 단일화 필요
SecurityConfig와 CorsMvcConfig에서 서로 다른 CORS 정책을 정의하고 있어, Spring Security가 우선 적용되는 상황에서 예기치 않은 동작 및 디버깅 난이도가 증가합니다. 전역 CORS 설정을 공통 Bean으로 추출하거나, MVC 설정을 제거하고 SecurityConfig에서만 관리해야 합니다.
점검 위치
- src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java: addCorsMappings 메서드
- src/main/java/hello/cluebackend/global/config/SecurityConfig.java: corsCustomizer.configurationSource 정의
제안 리팩터링
- 공통 CorsConfigurationSource @bean 생성
- SecurityConfig와 WebMvcConfigurer가 동일한 Bean 참조
- 또는 CorsMvcConfig의 addCorsMappings 제거 후 SecurityConfig에서만 CORS 관리
예시 변경안
# src/main/java/hello/cluebackend/global/config/CorsConfig.java
@Configuration
public class CorsConfig {
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000", "http://10.150.149.87:7789"));
+ config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
+ config.setAllowedHeaders(Arrays.asList("*"));
+ config.setExposedHeaders(Arrays.asList("Authorization","Set-Cookie"));
+ config.setAllowCredentials(true);
+ config.setMaxAge(3600L);
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+ return source;
+ }
}
# src/main/java/hello/cluebackend/global/config/SecurityConfig.java
http
- .cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() { … }))
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
// csrf disable
.csrf(auth -> auth.disable());
# src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java
- @Override
- public void addCorsMappings(CorsRegistry registry) { … }
+ // CORS 관리는 CorsConfig의 corsConfigurationSource()로 단일화Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java around lines
10-19 there is a duplicate CORS policy that conflicts with SecurityConfig;
remove duplication by centralizing the CORS configuration: create a single
CorsConfigurationSource @Bean (e.g., in a common config class or SecurityConfig)
and have both SecurityConfig (corsCustomizer.configurationSource) and any
WebMvcConfigurer reference that Bean, or alternatively delete the
CorsMvcConfig.addCorsMappings method and let SecurityConfig fully manage CORS;
ensure the single shared CorsConfigurationSource contains the intended
allowedOriginPatterns, methods, headers, exposed headers, and allowCredentials
settings and is injected where needed.
| public void addCorsMappings(CorsRegistry registry) { | ||
| registry.addMapping("/**") // 모든 API 경로 허용 | ||
| // .allowedOriginPatterns("http://10.150.149.87:7789") // 프론트 IP + 포트 | ||
| .allowedOriginPatterns("*") |
There was a problem hiding this comment.
🛠️ Refactor suggestion
자격증명 허용 상태에서 allowedOriginPatterns("*") 사용은 보안상 취약합니다.
allowCredentials(true)와 함께 "*"를 허용하면 모든 오리진에 대해 인증 쿠키/자격증명이 전송됩니다. Spring은 이 경우 요청 Origin을 그대로 반사해 사실상 무제한 허용이 됩니다. 운영/개발 오리진을 명시적 화이트리스트로 제한하세요. 현재 SecurityConfig는 "http://localhost:3000"만 허용 중이라 불일치도 있습니다.
권장 변경(예: SecurityConfig와 일치):
- .allowedOriginPatterns("*")
+ .allowedOriginPatterns("http://localhost:3000")여러 오리진이 필요하면 프로퍼티로 주입하거나 패턴을 구체화하세요(예: "https://*.example.com").
Also applies to: 18-18
🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java around lines
14 and 18, using allowedOriginPatterns("*") together with allowCredentials(true)
is insecure and inconsistent with SecurityConfig; replace the wildcard with an
explicit whitelist that matches SecurityConfig (e.g. "http://localhost:3000") or
inject allowed origins from application properties, and use allowedOrigins(...)
or specific allowedOriginPatterns (e.g. "https://*.example.com") to restrict
origins while keeping credentials enabled.
| .allowedOriginPatterns("*") | ||
| .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드 | ||
| .allowedHeaders("*") // 모든 요청 헤더 허용 | ||
| .exposedHeaders("Authorization", "Set-Cookie") // 클라이언트가 접근 가능한 헤더 |
There was a problem hiding this comment.
Set-Cookie는 CORS로 노출 불가한 금지 응답 헤더입니다. 제거하세요.
Access-Control-Expose-Headers에 "Set-Cookie"를 추가해도 브라우저에서 읽을 수 없습니다. 쿠키는 credentials 옵션으로만 전달/저장됩니다. 남겨두면 오해만 유발합니다.
- .exposedHeaders("Authorization", "Set-Cookie") // 클라이언트가 접근 가능한 헤더
+ .exposedHeaders("Authorization") // 응답에서 읽어야 하는 헤더만 노출🤖 Prompt for AI Agents
In src/main/java/hello/cluebackend/global/config/CorsMvcConfig.java around line
17, remove "Set-Cookie" from the Access-Control-Expose-Headers list (it cannot
be exposed via CORS). Leave other exposed headers like "Authorization" intact,
and if you intend to send cookies across origins ensure cors configuration uses
allowCredentials(true) and properly specified
allowedOrigins/allowedOriginPatterns so cookies are sent via credentials rather
than attempting to expose Set-Cookie.
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java (1)
87-101: 첨부 다운로드: 권한 검증 누락 및 Content-Type 파싱 예외 처리 필요.이전 리뷰에서 제안된 다운로드 권한 검증이 아직 없습니다. S3/스토리지 접근 전 멤버십을 확인하고, 잘못된 MIME으로
parseMediaType가 터질 수 있으니 fallback을 두세요.) throws IOException { AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrderThrow(assignmentAttachmentId); - Resource resource = assignmentCommandService.downloadAttachment(assignmentAttachment); + // Storage 접근 전에 권한 검증 + Long classroomId = assignmentAttachment.getAssignment().getClassRoom().getClassRoomId(); + if (!classroomUserService.isUserInClassroom(classroomId, userId)) { + throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); + } String original = assignmentAttachment.getOriginalFileName(); String contentType = assignmentAttachment.getContentType(); - MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM; + MediaType mediaType; + try { + mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM; + } catch (org.springframework.http.InvalidMediaTypeException ex) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + // 권한 검증 후 실제 다운로드 + Resource resource = assignmentCommandService.downloadAttachment(assignmentAttachment); return ResponseEntity.ok() .contentType(mediaType) .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment() .filename(original, StandardCharsets.UTF_8) .build() .toString()) .body(resource); }
🧹 Nitpick comments (9)
src/main/java/hello/cluebackend/global/config/S3Config.java (4)
22-29: 구현체 반환(AmazonS3Client)과 다운캐스트 지양 → 인터페이스(AmazonS3)로 노출빈 타입을 인터페이스로 노출하면 테스트/대체 구현이 쉬워지고, 빌더 반환값에 대한 불필요한 캐스팅을 제거할 수 있습니다.
+import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.AmazonS3ClientBuilder; @@ - public AmazonS3Client amazonS3Client() { + public AmazonS3 amazonS3Client() { @@ - return (AmazonS3Client) AmazonS3ClientBuilder.standard() + return AmazonS3ClientBuilder.standard() .withRegion(region) .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) .build();
13-18: 고정 키 주입 대신 기본 자격증명 체인 사용 제안애플리케이션 외부(ENV, EC2/ECS/IRSA, 프로파일 등)에서 안전하게 자격증명을 공급받도록
DefaultAWSCredentialsProviderChain사용을 권장합니다. 비밀키 프로퍼티 의존도를 줄이고 보안/운영성을 개선합니다.-import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; @@ - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; + // 필요 시 여전히 프로퍼티 기반으로 대체 가능하나, 기본 체인을 우선 사용 권장 @@ - BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey); return (AmazonS3Client) AmazonS3ClientBuilder.standard() .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) + .withCredentials(DefaultAWSCredentialsProviderChain.getInstance()) .build();Also applies to: 24-28
22-29: 간단한 스모크 테스트 추가 권장S3 클라이언트 빈 생성/리전 주입을 검증하는 스프링 통합 테스트를 추가하면 설정 회귀를 빠르게 감지할 수 있습니다. 원하시면 테스트 템플릿 드리겠습니다.
3-7: AWS SDK v1/v2 혼용 정리 필요
- 코드에서는 com.amazonaws.* (v1) API를 사용하고 있으나, build.gradle에는 software.amazon.awssdk:s3 (v2) 의존성과 함께 spring-cloud-starter-aws(내부적으로 v1 사용) 의존성이 모두 포함되어 있습니다. 빌드·런타임에 의도한 SDK가 로드되는지 확인하거나, v1 또는 v2 중 하나로 통일해 의존성을 정리하거나 마이그레이션하세요. [build.gradle:68–70]
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java (3)
74-76: 엔드포인트/메서드 명명 명확화 제안.URI가 단수(
/attachment)이고 메서드명은findAllAssignments로 혼동됩니다. 복수형과 의미를 맞추면 가독성이 좋아집니다. (외부 호환성 영향 검토 필요)- @GetMapping("/{submissionId}/attachment") - public ResponseEntity<List<SubmissionAttachmentDto>> findAllAssignments(@CurrentUser Long userId, @PathVariable Long submissionId) { + @GetMapping("/{submissionId}/attachments") + public ResponseEntity<List<SubmissionAttachmentDto>> getSubmissionAttachments(@CurrentUser Long userId, @PathVariable Long submissionId) {
3-3: 와일드카드 import 지양.충돌/가독성 문제를 줄이기 위해 사용한 DTO만 명시적으로 import 하세요.
-import hello.cluebackend.domain.assignment.api.dto.response.*; +import hello.cluebackend.domain.assignment.api.dto.response.AssignmentResponseDto; +import hello.cluebackend.domain.assignment.api.dto.response.GetAllAssignmentDto;
28-36: 컨트롤러/서비스 네이밍과 역할 혼선 정리 제안.이 컨트롤러는 전부 조회(GET)인데 클래스/서비스 명은
Command입니다. 현재AssignmentQueryController가 생성/수정/삭제(명령)를 담당하고 있어 CQRS 관점에서 명칭과 역할이 뒤바뀐 상태입니다. 유지보수성 위해 클래스/빈 명칭을 역할에 맞게 정렬하거나 단일 컨트롤러로 통합을 검토하세요.src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java (2)
10-12: 엔티티에 @AllArgsConstructor는 지양JPA 엔티티에서 공개 전체생성자는 불변식 우회 생성 위험이 큽니다. 빌더/정적 팩토리로만 생성되도록 제한하세요.
권장 변경:
-@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class AssignmentAttachment extends BaseEntity {필요 시 정적 팩토리 제공:
public static AssignmentAttachment fromFile(Assignment a, String key, String name, String contentType, long size) { ... } public static AssignmentAttachment fromUrl(Assignment a, String url) { ... }
12-12: 쿼리 성능 대비 인덱스/NotNull 제약 보강 권장첨부는 과제 기준으로 자주 조회됩니다. (assignment_id), (assignment_id, file_type) 인덱스와 FK NotNull을 권장합니다.
참고(표현만, 적용 시 Table/JoinColumn에 반영 필요):
@Table( name = "assignment_attachment", indexes = { @Index(name = "idx_attachment_assignment", columnList = "assignment_id"), @Index(name = "idx_attachment_assignment_type", columnList = "assignment_id,file_type") } ) // ... @ManyToOne(fetch = LAZY) @JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
src/main/generated/hello/cluebackend/domain/assignment/domain/QAssignmentAttachment.javais excluded by!**/generated/**
📒 Files selected for processing (9)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/FileType.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/exception/AccessDeniedException.java(1 hunks)src/main/java/hello/cluebackend/global/config/S3Config.java(1 hunks)src/main/resources/application.yaml(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/main/java/hello/cluebackend/domain/assignment/exception/AccessDeniedException.java
- src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java
- src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java
- src/main/resources/application.yaml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-26T14:23:15.065Z
Learnt from: Hgyeol
PR: EscapeFrame/clueBackend#48
File: src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java:3-5
Timestamp: 2025-08-26T14:23:15.065Z
Learning: AssignmentAttachmentDto has two different versions: request package contains only String url for URL uploads, while response package contains full metadata (fileType, value, originalFileName, contentType, size) for responses. The controller and service correctly use request DTO for uploads and response DTO for queries.
Applied to files:
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java
🧬 Code graph analysis (3)
src/main/java/hello/cluebackend/domain/assignment/domain/FileType.java (3)
src/main/java/hello/cluebackend/domain/assignment/domain/SubmitType.java (1)
SubmitType(3-7)src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentContent.java (1)
AssignmentContent(9-46)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/AssignmentAttachmentDto.java (1)
AssignmentAttachmentDto(10-26)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java (3)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentQueryController.java (1)
RestController(20-96)src/main/java/hello/cluebackend/domain/submission/api/SubmissionCommandController.java (1)
RestController(22-73)src/main/java/hello/cluebackend/domain/assignment/service/AssignmentService.java (2)
AssignmentService(43-347)createAssignment(134-216)
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java (5)
src/main/java/hello/cluebackend/domain/assignment/presentation/dto/AssignmentAttachmentDto.java (1)
AssignmentAttachmentDto(10-26)src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentContent.java (1)
AssignmentContent(9-46)src/main/java/hello/cluebackend/domain/assignment/domain/repository/AssignmentAttachmentRepository.java (1)
AssignmentAttachmentRepository(10-13)src/main/java/hello/cluebackend/domain/user/domain/UserEntity.java (1)
UserEntity(13-67)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/Assignmentfile.java (1)
Assignmentfile(3-7)
🔇 Additional comments (2)
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java (1)
1-102: AccessDeniedException 예외 매핑 확인 필요 글로벌 예외 처리기(@ControllerAdvice)에AccessDeniedException이 HTTP 403으로 매핑되도록@ExceptionHandler또는@ResponseStatus(HttpStatus.FORBIDDEN)설정 여부를 검증해주세요.src/main/java/hello/cluebackend/domain/assignment/domain/FileType.java (1)
3-5: 중복 enum(FileType vs fileType) 통합 또는 명확한 네임스페이스 분리 필요. 레거시 SubmitType/LINK/IMAGE 참조는 코드상 발견되지 않으나, 데이터베이스 마이그레이션 계획 유무를 직접 검증해 주세요.
| AssignmentResponseDto result = assignmentCommandService.findById(assignmentId); | ||
| Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); | ||
| Long classroomId = assignment.getClassRoom().getClassRoomId(); | ||
| if (!classroomUserService.isUserInClassroom(classroomId, userId)) { | ||
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | ||
| } | ||
| return ResponseEntity.ok(result); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
권한 검증을 DTO 조회 전에 수행하세요.
권한 확인 전에 DTO를 먼저 조회하면 불필요한 DB 접근과 정보 노출 가능성이 있습니다. 검증 후 조회로 순서를 바꾸세요.
- AssignmentResponseDto result = assignmentCommandService.findById(assignmentId);
- Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId);
- Long classroomId = assignment.getClassRoom().getClassRoomId();
- if (!classroomUserService.isUserInClassroom(classroomId, userId)) {
- throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다.");
- }
- return ResponseEntity.ok(result);
+ Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId);
+ Long classroomId = assignment.getClassRoom().getClassRoomId();
+ if (!classroomUserService.isUserInClassroom(classroomId, userId)) {
+ throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다.");
+ }
+ // 권한 검증 후 DTO 조회
+ AssignmentResponseDto result = assignmentCommandService.findById(assignmentId);
+ return ResponseEntity.ok(result);📝 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.
| AssignmentResponseDto result = assignmentCommandService.findById(assignmentId); | |
| Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); | |
| Long classroomId = assignment.getClassRoom().getClassRoomId(); | |
| if (!classroomUserService.isUserInClassroom(classroomId, userId)) { | |
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | |
| } | |
| return ResponseEntity.ok(result); | |
| } | |
| // 먼저 도메인 객체 로드 및 권한 검증 | |
| Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); | |
| Long classroomId = assignment.getClassRoom().getClassRoomId(); | |
| if (!classroomUserService.isUserInClassroom(classroomId, userId)) { | |
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | |
| } | |
| // 권한 검증 후 DTO 조회 | |
| AssignmentResponseDto result = assignmentCommandService.findById(assignmentId); | |
| return ResponseEntity.ok(result); |
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
around lines 43-50, the code retrieves the AssignmentResponseDto before
performing the classroom membership check, causing unnecessary DB access and
potential info exposure; change the flow to first load the Assignment entity (or
call findByIdOrThrow) to obtain classroomId, perform
classroomUserService.isUserInClassroom(...) and throw AccessDeniedException if
the user is not in the class, and only after the permission check obtain or
construct the AssignmentResponseDto (either by calling findById or converting
the already-loaded Assignment), so permission verification happens before DTO
retrieval.
| List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId,classId); | ||
|
|
||
| if (!classroomUserService.isUserInClassroom(classId, userId)) { | ||
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | ||
| } | ||
|
|
||
| return ResponseEntity.ok(result); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
교실 과제 전체 조회: 권한 체크를 서비스 호출 전에 수행하세요.
현재는 결과를 먼저 조회한 뒤 권한을 확인합니다. 순서를 반대로 하여 불필요한 쿼리를 피하고 일관된 응답(403)을 보장하세요. 또한 findAllById는 의미가 모호하므로 findAllByClassIdForUser 등으로 시그니처/네이밍 정리 검토 바랍니다.
- List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId,classId);
-
- if (!classroomUserService.isUserInClassroom(classId, userId)) {
- throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다.");
- }
+ if (!classroomUserService.isUserInClassroom(classId, userId)) {
+ throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다.");
+ }
+ List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId, classId);
return ResponseEntity.ok(result);📝 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.
| List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId,classId); | |
| if (!classroomUserService.isUserInClassroom(classId, userId)) { | |
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | |
| } | |
| return ResponseEntity.ok(result); | |
| } | |
| // 권한을 먼저 검증하여 불필요한 조회를 방지합니다. | |
| if (!classroomUserService.isUserInClassroom(classId, userId)) { | |
| throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); | |
| } | |
| List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId, classId); | |
| return ResponseEntity.ok(result); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
around lines 58 to 65, the controller calls assignmentCommandService.findAllById
before checking classroom membership; move the
classroomUserService.isUserInClassroom(classId, userId) check to happen first
and throw AccessDeniedException if false to avoid unnecessary queries and ensure
consistent 403 responses, then call the service to fetch assignments;
additionally rename the service method from findAllById to a clearer name such
as findAllByClassIdForUser (and update its signature/implementations) to reflect
intent and reduce ambiguity.
| @GetMapping("/{submissionId}/attachment") | ||
| public ResponseEntity<List<SubmissionAttachmentDto>> findAllAssignments(@CurrentUser Long userId, @PathVariable Long submissionId) { | ||
| List<SubmissionAttachmentDto> result = submissionCommandService.findAllAssignment(submissionId); | ||
| return ResponseEntity.ok(result); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
제출 첨부 전체 조회: 권한 검증 누락입니다.
submissionId만으로 첨부를 누구나 열람할 수 있습니다. 제출이 속한 교실을 역추적해 멤버십(또는 담당 교사/본인) 검증을 추가하세요. 서비스 계층에서의 검증도 병행 권장.
public ResponseEntity<List<SubmissionAttachmentDto>> findAllAssignments(@CurrentUser Long userId, @PathVariable Long submissionId) {
- List<SubmissionAttachmentDto> result = submissionCommandService.findAllAssignment(submissionId);
+ // 제출 → 과제 → 교실 역추적 후 권한 검증
+ Long classroomId = submissionCommandService.findClassroomIdBySubmissionId(submissionId); // 없으면 서비스에 추가 필요
+ if (!classroomUserService.isUserInClassroom(classroomId, userId)) {
+ throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다.");
+ }
+ List<SubmissionAttachmentDto> result = submissionCommandService.findAllAssignment(submissionId);
return ResponseEntity.ok(result);
}검증 존재 여부 확인 스크립트:
🏁 Script executed:
#!/bin/bash
# SubmissionCommandService 내에서 사용자/멤버십 검증이 수행되는지 확인
rg -n -C3 -g '!**/build/**' -P 'class\s+SubmissionCommandService|findAllAssignment\s*\(|findClassroomIdBySubmissionId\s*\('Length of output: 3085
권한 검증 추가
AssignmentCommandController#findAllAssignments가 submissionId만으로 첨부를 조회합니다. 제출→과제→교실을 역추적해 userId의 멤버십(또는 담당 교사/본인) 여부를 검증하고, 서비스 레이어(SubmissionCommandService)에서도 동일 검증을 병행하세요.
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/api/AssignmentCommandController.java
around lines 75-79, the controller currently returns attachments using only
submissionId without authorization; update the controller to first resolve
submission -> assignment -> classroom and verify the CurrentUser (userId) is
either the submission owner, a teacher for that classroom, or a classroom member
before calling submissionCommandService.findAllAssignment; additionally add the
same permission check inside SubmissionCommandService (or a shared
permission/validator component) so service-layer calls also enforce the
membership/role check and throw an appropriate 403/AccessDenied when
unauthorized; implement the lookup using existing repositories (submission ->
assignment -> classroom) or a lightweight DTO query and reuse the check method
from the service to avoid duplication.
| // FILE, URL | ||
| @Column(nullable = false) | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "submit_type") | ||
| private SubmitType submitType; | ||
| private FileType type; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
컬럼명 'type'은 충돌 소지 높음 — 명시적 컬럼명으로 변경 권장
RDB별 예약어/키워드와 충돌 가능성이 있으므로 컬럼명을 명시적으로 지정하는 편이 안전합니다(예: file_type).
다음 수정 제안:
- @Column(nullable = false)
+ @Column(name = "file_type", nullable = false)
@Enumerated(EnumType.STRING)
private FileType type;📝 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.
| // FILE, URL | |
| @Column(nullable = false) | |
| @Enumerated(EnumType.STRING) | |
| @Column(name = "submit_type") | |
| private SubmitType submitType; | |
| private FileType type; | |
| // FILE, URL | |
| @Column(name = "file_type", nullable = false) | |
| @Enumerated(EnumType.STRING) | |
| private FileType type; |
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java
around lines 21 to 24, the column name 'type' is prone to conflicts with RDB
reserved words; change the JPA mapping to explicitly set a safer column name
(e.g., file_type) by updating the @Column annotation to include name="file_type"
while preserving nullable=false and the @Enumerated(EnumType.STRING) annotation;
also ensure any JPQL/native queries, DTOs, or migration scripts referencing the
old column are updated accordingly.
| // 실제 파일이면 S3 Key, URL이면 링크 | ||
| @Column(nullable = false) | ||
| private String value; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
'value' 필드: 의미 모호·URL 길이 한계·빈문자 허용 문제
- 컬럼명 'value'는 의미가 불명확하고 일부 DB/도구에서 혼동됩니다.
- URL/S3 키는 255자를 초과할 수 있습니다(최대 수천자). length를 늘리고 공백 방지 @notblank 추가 권장.
다음 수정 제안(필드명은 유지, 컬럼명만 변경):
- @Column(nullable = false)
- private String value;
+ @jakarta.validation.constraints.NotBlank
+ @Column(name = "reference", nullable = false, length = 2048)
+ private String value;🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java
around lines 26 to 28, the 'value' column name is ambiguous, allows blank
strings and may be too short for long URLs/S3 keys; update the JPA mapping to
keep the field name 'value' but set a clearer column name (e.g.,
"attachment_value"), increase the column length to support long URLs/keys (e.g.,
length=4096 or use @Lob if preferred), and add a validation annotation @NotBlank
to prevent empty/blank values while keeping nullable=false.
| // 파일일 경우 메타데이터 | ||
| private String originalFileName; | ||
| private String contentType; | ||
| private Long size; |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
메타데이터 컬럼 제약 보강 + 다운스트림 컴파일/런타임 호환성 이슈 존재
- 원본 파일명/콘텐츠 타입 길이 제약 추가 권장.
- size 타입을 Long으로 변경했으므로 기존 서비스/DTO가 int 기반(getFileSize)일 경우 컴파일 오류 및 오버플로 리스크.
다음 엔티티 최소 제약 추가:
- private String originalFileName;
- private String contentType;
- private Long size;
+ @Column(length = 512)
+ private String originalFileName;
+ @Column(length = 255)
+ private String contentType;
+ @jakarta.validation.constraints.PositiveOrZero
+ private Long size;다운스트림 수정 제안(다른 파일, 참고용):
// src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/Assignmentfile.java
public record Assignmentfile(
Long fileId,
String fileName,
Long fileSize // int -> Long 권장
) {}// src/main/java/hello/cluebackend/domain/assignment/service/AssignmentService.java
attachment -> new Assignmentfile(
attachment.getAssignmentAttachmentId(),
attachment.getOriginalFileName(),
attachment.getSize() // getFileSize() -> getSize()
)레거시 참조/컴파일 브레이크 탐지 스크립트:
🏁 Script executed:
#!/bin/bash
# getFileSize/field명/타입 불일치 탐지
rg -nP --type=java -C2 '\bgetFileSize\s*\(|\bfileSize\b'
# Assignmentfile 시그니처 및 생성부 점검
rg -nP --type=java -C2 'record\s+Assignmentfile\s*\(|new\s+Assignmentfile\s*\('Length of output: 140
메타데이터 컬럼 제약 추가:
originalFileName에 @Column(length=512), contentType에 @Column(length=255) 제약을, size에는 @PositiveOrZero 유효성 어노테이션을 적용하세요.
🤖 Prompt for AI Agents
In
src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java
around lines 30 to 33, add JPA column length constraints and a validation
annotation: annotate originalFileName with @Column(length = 512), annotate
contentType with @Column(length = 255), and annotate size with @PositiveOrZero;
also add the necessary imports (javax.persistence.Column and
javax.validation.constraints.PositiveOrZero) so the class compiles.
Summary by CodeRabbit