Conversation
Walkthrough패키지 재구성(api → presentation), DTO 재배치/신규화, 컨트롤러 경로·반환타입 정리, 서비스 로직 리팩터링(팩토리 메서드 도입, 접근 제어 추가), 도메인 엔티티 변경(Submission에 ClassRoom 추가, BaseEntity 제거, FileType 대문자화), 레포지토리 메서드 확장, 개발 설정 변경(DDL auto update), 테스트 추가. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Controller as AssignmentCommandController
participant CmdService as AssignmentCommandService
participant UserSvc as UserService
participant Repo as Repositories
participant Mapper as DTO Factory
User->>Controller: GET /api/assignments/{id}
Controller->>CmdService: findByIdOrThrow(id)
CmdService->>Repo: Assignment findById(id)
Repo-->>CmdService: Assignment
Controller->>UserSvc: isUserInClassroom(userId, assignment.classRoomId)
alt not member
Controller-->>User: 403 Forbidden
else member
Controller->>CmdService: load attachments
CmdService->>Repo: find attachments
Repo-->>CmdService: List<AssignmentAttachment>
CmdService->>Mapper: AssignmentResponseDto.from(assignment, attachments)
Mapper-->>CmdService: AssignmentResponseDto
CmdService-->>Controller: AssignmentResponseDto
Controller-->>User: 200 OK + body
end
sequenceDiagram
autonumber
actor User
participant Controller as SubmissionCommandController
participant ClassUserSvc as ClassroomUserService
participant CmdService as SubmissionCommandService
participant Repo as Repositories
participant Mapper as DTOs
User->>Controller: GET /api/submissions/{classId}
Controller->>ClassUserSvc: isUserInClassroom(classId, userId)
alt not member
Controller-->>User: 403 Forbidden
else member
Controller->>CmdService: findAllByAssignmentId(userId, classId)
CmdService->>Repo: findAllByClassRoomAndUser(class, user)
Repo-->>CmdService: List<Submission>
loop for each submission
CmdService->>Repo: findAllBySubmissionId(submissionId)
Repo-->>CmdService: List<SubmissionAttachment>
CmdService->>Mapper: map to SubmissionAttachmentResponse
end
CmdService-->>Controller: List<SubmissionResponse>
Controller-->>User: 200 OK + body
end
sequenceDiagram
autonumber
actor User
participant Controller as SubmissionCommandController
participant CmdService as SubmissionCommandService
participant Storage as File Storage
User->>Controller: GET /api/submissions/{attachmentId}/download
Controller->>CmdService: findSubmissionAttachmentByIdOrThrow(attachmentId)
CmdService-->>Controller: SubmissionAttachment
Controller->>CmdService: downloadAttachment(attachment)
alt type == FILE
CmdService->>Storage: load file
Storage-->>CmdService: Resource
else type == URL
CmdService-->>Controller: Resource (redirect/stream URL)
end
CmdService-->>Controller: Resource
Controller-->>User: 200 OK (Content-Disposition, Content-Type)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (13)
src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentQueryController.java (1)
42-96: 권한 검증 누락 — 서비스 시그니처에 userId 전달을 강제하세요AssignmentQueryService에 userId 파라미터가 없습니다. 컨트롤러에서 userId를 서비스로 전달해 소유자/권한 체크를 서비스 레벨에서 강제하도록 시그니처를 변경하세요.
확인된 시그니처 (파일: src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java):
- public void delete(UUID assignmentId)
- public UUID patchAssignment(UUID assignmentId, ModifyAssignmentDto dto)
- public void uploadUrlAttachment(UUID assignmentId, List dtos)
- public void uploadFileAttachment(UUID assignmentId, MultipartFile file)
- public void deleteAttachment(UUID attachmentId)
권장 조치: 컨트롤러에서 assignmentQueryService.delete(userId, assignmentId) 등으로 호출하도록 변경하고, 서비스에 userId 인자를 추가해 소유자/권한 검증을 구현하세요.
선택 권장: 다건 파일 업로드는 부분 성공 가능성이 있으므로 리스트/배열을 받아 단일 트랜잭션으로 처리하는 방식 검토하세요.
src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java (1)
88-102: 트랜잭션 누락 및 파일-DB 간 보상 처리 부족파일 저장 후 DB 실패 시 파일 고아 발생. 메서드에
@Transactional추가 및 롤백 시 파일 삭제 보상 처리 필요.// 첨부 파일 추가 - public void uploadFileAttachment(UUID assignmentId, MultipartFile file) { + @Transactional + public void uploadFileAttachment(UUID assignmentId, MultipartFile file) { Assignment assignment = assignmentCommandService.findByIdOrThrow(assignmentId); String storedFileName = fileService.storeFile(file); @@ assignmentAttachmentRepository.save(result); }보상 처리 예시(개념):
- 트랜잭션 롤백 시
storedFileName삭제 등록 (TransactionSynchronizationManager.registerSynchronization).src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java (1)
40-46: 권한 검증 누락 및 N+1 쿼리 — 즉시 수정 필요
- 문제(위치): src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java — findAllById(UUID userId, UUID classId) (라인 40–46): userId가 사용되지 않아 권한/소속 검증이 없음. assignments.stream().map(a -> findById(...)) 호출로 인해 첨부 조회가 N+1 발생.
- 조치(우선순위):
- 권한 검증 추가: classRoomService.verifyMemberOrThrow(userId, classId) 또는 repository에서 user 기준 필터 적용.
- N+1 해소: repository 레벨 fetch-join(첨부/작성자 포함)으로 한 번에 조회하거나 assignments 조회 후 assignmentAttachmentRepository.findAllByAssignmentIn(...) 등으로 일괄 조회→그룹핑하여 DTO 구성(스트림 내 개별 findById 호출 제거).
src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionQueryController.java (2)
56-64: 멀티파트 파일 업로드 매핑 오용(@RequestBody MultipartFile).
MultipartFile은@RequestBody가 아니라@RequestPart(또는@RequestParam)와consumes = multipart/form-data를 사용해야 정상 바인딩됩니다. 또한 이 API도 권한 검증을 위해userId를 서비스에 전달하세요.- @PostMapping("/{submissionId}/file") - public ResponseEntity<SubmissionAttachment> fileUpload( + @PostMapping(value = "/{submissionId}/file", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity<SubmissionAttachment> fileUpload( @CurrentUser UUID userId, @PathVariable UUID submissionId, - @RequestBody MultipartFile file + @org.springframework.web.bind.annotation.RequestPart("file") MultipartFile file ) throws IOException { - SubmissionAttachment submissionAttachment = submissionQueryService.fileUpload(submissionId, file); + SubmissionAttachment submissionAttachment = submissionQueryService.fileUpload(userId, submissionId, file); return ResponseEntity.ok(submissionAttachment); }추가로 응답은 도메인 엔티티 대신 프레젠테이션 DTO 사용을 권장합니다(아래 별도 코멘트).
67-75: 경로 변수 철자 오류 및 PathVariable 매핑 불일치, 응답 메시지도 부정확합니다.
@PostMapping("/{submisisonId}/link")오타로 인해@PathVariable UUID submissionId가 매핑되지 않아 400/500이 발생합니다. 또한 성공 메시지가 “첨부 파일 삭제”로 잘못되어 있습니다. 권한 검증도 동일하게 필요합니다.- @PostMapping("/{submisisonId}/link") + @PostMapping("/{submissionId}/link") public ResponseEntity<?> linkUpload( @CurrentUser UUID userId, @PathVariable UUID submissionId, @RequestBody SubmissionAttachmentUrlDto dto ) { - submissionQueryService.linkUpload(submissionId, dto); - return ResponseEntity.ok("첨부 파일 삭제"); + submissionQueryService.linkUpload(userId, submissionId, dto); + return ResponseEntity.ok("첨부 링크가 성공적으로 추가되었습니다."); }src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentCommandController.java (3)
54-67: 권한 체크 순서가 역전되어 데이터 조회 선행(TOCTOU/불필요 조회).
findAllById로 데이터를 조회한 뒤에 교실 멤버십을 검사합니다. 권한 체크를 먼저 수행해 불필요한 조회/잠재적 정보 노출을 방지하세요.@GetMapping("/{classId}/all") public ResponseEntity<List<AssignmentResponseDto>> getAllClassroomAssignment( @CurrentUser UUID userId, @PathVariable UUID classId ) { - List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId,classId); - - if (!classroomUserService.isUserInClassroom(classId, userId)) { + if (!classroomUserService.isUserInClassroom(classId, userId)) { throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); } + List<AssignmentResponseDto> result = assignmentCommandService.findAllById(userId, classId); return ResponseEntity.ok(result); }
82-82: 오탈자:findAssignmentAttachmentByIdOrderThrow→findAssignmentAttachmentByIdOrThrow.컴파일 에러 가능성이 큽니다. 메서드명을 정정하세요.
- AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrderThrow(assignmentAttachmentId); + AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrThrow(assignmentAttachmentId);
76-96: 첨부 다운로드도 권한 체크 필요.첨부가 속한 과제의 교실 멤버인지 확인 후 응답하세요.
public ResponseEntity<Resource> assignmentAttachmentDownload( @CurrentUser UUID userId, @PathVariable UUID assignmentAttachmentId ) throws IOException { - AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrderThrow(assignmentAttachmentId); + AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrThrow(assignmentAttachmentId); + + UUID classroomId = assignmentAttachment.getAssignment().getClassRoom().getClassRoomId(); + if (!classroomUserService.isUserInClassroom(classroomId, userId)) { + throw new AccessDeniedException("해당 수업실에 속하지 않은 유저입니다."); + }src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java (3)
62-77: 파일 업로드: 권한 검증 + 변수 오탈자 + 기본 검증(용량/타입) 추가 권장.
- 소유자/권한 검증이 없습니다.
storedFiledName오탈자입니다.- 업로드 제한(최대 크기/허용 MIME) 검증이 없으면 위험합니다.
- public SubmissionAttachment fileUpload(UUID submissionId, MultipartFile file) { + public SubmissionAttachment fileUpload(UUID userId, UUID submissionId, MultipartFile file) { Submission submission = submissionCommandService.findByIdOrThrow(submissionId); + if (!submission.getUser().getUserId().equals(userId)) { + throw new hello.cluebackend.domain.assignment.exception.AccessDeniedException("본인 제출물에만 첨부할 수 있습니다."); + } + + // 간단 검증 예시(세부 정책에 맞게 조정) + if (file.isEmpty()) throw new IllegalArgumentException("빈 파일은 업로드할 수 없습니다."); + if (file.getSize() > 20L * 1024 * 1024) throw new IllegalArgumentException("최대 20MB까지 업로드 가능합니다."); - String storedFiledName = fileService.storeFile(file); + String storedFileName = fileService.storeFile(file); SubmissionAttachment result = SubmissionAttachment.builder() .submission(submission) .type(FileType.FILE) - .value(storedFiledName) + .value(storedFileName) .originalFileName(file.getOriginalFilename()) .contentType(file.getContentType()) .size(file.getSize()) .build(); submissionAttachmentRepository.save(result); return result; }
81-92: 링크 첨부: 권한 검증 및 입력값 검증 필요.URL 형식/길이 체크와 소유자 검증을 추가하세요.
- public SubmissionAttachment linkUpload(UUID submissionId, SubmissionAttachmentUrlDto dto) { + public SubmissionAttachment linkUpload(UUID userId, UUID submissionId, SubmissionAttachmentUrlDto dto) { Submission submission = submissionCommandService.findByIdOrThrow(submissionId); + if (!submission.getUser().getUserId().equals(userId)) { + throw new hello.cluebackend.domain.assignment.exception.AccessDeniedException("본인 제출물에만 첨부할 수 있습니다."); + } + if (dto == null || dto.url() == null || dto.url().isBlank()) { + throw new IllegalArgumentException("유효한 URL이 필요합니다."); + } SubmissionAttachment submissionAttachment = SubmissionAttachment.builder() .submission(submission) .type(FileType.URL) .value(dto.url()) .build();
96-102: 첨부 삭제도 권한 검증 필요.첨부의 소유자 또는 교사만 삭제 가능하도록
userId로 검사하세요.- public void deleteSubmissionAttachment(UUID submissionAttachmentId) { - SubmissionAttachment submissionAttachment = submissionCommandService.findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); + public void deleteSubmissionAttachment(UUID userId, UUID submissionAttachmentId) { + SubmissionAttachment submissionAttachment = + submissionCommandService.findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); + if (!submissionAttachment.getSubmission().getUser().getUserId().equals(userId)) { + throw new hello.cluebackend.domain.assignment.exception.AccessDeniedException("삭제 권한이 없습니다."); + } if(submissionAttachment.getType() == FileType.FILE){ fileService.deleteFile(submissionAttachment.getValue()); } submissionAttachmentRepository.delete(submissionAttachment); }src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionCommandController.java (2)
52-59: 기능/의도 불일치: "전체 학생 과제 제출 여부"인데 현재 구현은 요청자(userId)로 필터링컨트롤러는 "전체"를 암시하나 서비스는
userId로 필터하여 요청자 본인만 반환합니다. 교사 권한 확인 후 전체 학생 결과를 반환하도록 서비스 수정이 필요합니다.
62-81: 첨부파일 다운로드에 접근제어 부재 → IDOR 취약점
submissionAttachmentId만 알면 소유자/교사 검증 없이 다운로드가 가능합니다. 사용자 소유이거나 요청자가 교사일 때만 허용하도록 검사 추가가 필요합니다.다음과 같이 간단히 방어 로직을 삽입하세요(서비스에 어서션 메서드 추가 전제).
@@ ) throws IOException { - SubmissionAttachment submissionAttachment = submissionCommandService.findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); + SubmissionAttachment submissionAttachment = submissionCommandService.findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); + submissionCommandService.assertCanAccessAttachment(userId, submissionAttachment); @@ - MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.ALL; + MediaType mediaType; + try { + mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM; + } catch (IllegalArgumentException ex) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + }서비스 측 보조 메서드는 아래 코멘트에 포함했습니다.
♻️ Duplicate comments (1)
src/main/java/hello/cluebackend/domain/submission/domain/FileType.java (1)
3-6: Enum 상수 대문자화 자체는 👍, 단 DB 마이그레이션 동반 필요위 파일에서 언급한 대로 문자열 기반 enum 저장을 사용하므로, 운영 데이터가 있다면 반드시 값 치환 또는 컨버터가 필요합니다. 상세 대응은 앞선 코멘트를 참고하세요.
🧹 Nitpick comments (44)
src/main/resources/application-dev.yml (1)
11-13: 루트 DEBUG 주석 처리 — 패키지/SQL 대상 로깅으로 대체 제안루트 레벨을 DEBUG로 두지 않는 건 👍. 대신 개발 편의를 위해 도메인/SQL만 선택적으로 DEBUG/TRACE 권장.
제안 diff:
-#logging: -# level: -# root: debug +logging: + level: + hello.cluebackend: debug + org.hibernate.SQL: debug + org.hibernate.type.descriptor.sql: tracesrc/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java (1)
16-20: 필드 명명 규칙: 대문자로 시작하는 필드명 수정 제안
private UUID SubmissionAttachmentId;는 자바 관례에 어긋납니다. Lombok/JSON 바인딩시 혼동 소지가 있어submissionAttachmentId로의 리팩터를 권장합니다(컬럼명은 동일하므로 스키마 변경 없음).- private UUID SubmissionAttachmentId; + private UUID submissionAttachmentId;src/main/java/hello/cluebackend/domain/assignment/persistence/AssignmentRepository.java (1)
24-24: 파생 쿼리 메서드 추가는 적합, 페이징/정렬 확장 고려
findAllByUserAndClassRoom은 컬렉션 폭증 시 응답 지연을 유발할 수 있습니다.Page<Assignment> findAllByUserAndClassRoom(UserEntity user, ClassRoom classRoom, Pageable pageable)추가를 권장합니다.+import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; ... - List<Assignment> findAllByUserAndClassRoom(UserEntity user, ClassRoom classRoom); + Page<Assignment> findAllByUserAndClassRoom(UserEntity user, ClassRoom classRoom, Pageable pageable);src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentQueryController.java (1)
12-12: 불필요한 로그 주입 제거 또는 사용
@Slf4j가 사용되지 않습니다. 사용 계획이 없다면 제거하세요.src/main/java/hello/cluebackend/domain/submission/presentation/dto/request/SubmissionAttachmentUrlDto.java (1)
3-5: 입력값 검증 추가 제안(URL 형식/공백 방지)요청 DTO에 검증 애너테이션을 추가해 조기 실패를 유도하세요.
package hello.cluebackend.domain.submission.presentation.dto.request; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.URL; + public record SubmissionAttachmentUrlDto( - String url + @NotBlank @URL String url ) { }src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java (1)
8-14: BaseEntity 제거에 따른 감시/감사 필드 손실 영향 확인감사(createdAt/updatedAt 등) 필드를 BaseEntity로부터 상속하던 경우, 제거로 인한 로깅/정렬/감사 요구사항 공백이 없는지 확인 바랍니다. 필요 시
@EntityListeners(AuditingEntityListener.class)+@CreatedDate/@LastModifiedDate로 대체하세요.원하시면 감사 필드/설정 추가 패치를 제안드리겠습니다.
src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/AssignmentAttachmentDto.java (1)
3-5: 요청 DTO에 Bean Validation 추가 권장빈/잘못된 URL 저장을 초기에 걸러주세요. 컨트롤러에서
@Valid와 함께 사용 가정.package hello.cluebackend.domain.assignment.presentation.dto.request; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.URL; + public record AssignmentAttachmentDto( - String url + @NotBlank @URL String url ) {}src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionDto.java (1)
5-11: 날짜 타입 직렬화 일관성
LocalDateTime의 타임존 모호성 회피를 위해 API 계층에서OffsetDateTime사용 또는@JsonFormat(timezone="UTC")명시 고려.src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java (3)
71-85: URL 첨부 유효성 검증 부재DTO에
@URL을 붙였더라도 서비스 레벨에서 빈 리스트/중복 URL에 대한 방어 로직이 있으면 안전합니다. 최소한 빈 컬렉션 early return 권장.
104-112: 파일 삭제 순서 및 트랜잭션 경계 개선현재 파일을 먼저 지우고 DB 삭제를 수행. DB 실패 시 참조 불일치 발생. DB 삭제를 트랜잭션 내에 먼저 수행하고, 커밋 후 파일 삭제(AfterCommit)로 이동하세요.
107-107: 오타로 보이는 메서드명
findAssignmentAttachmentByIdOrderThrow→...OrThrow추정. 혼동 방지 차원에서 수정 권장.src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/ModifyAssignmentDto.java (1)
13-18: PATCH DTO 검증 보완
- 두 날짜 모두 제공 시
startDate <= endDate보장 검증 필요.- 제목/내용의 공백만 입력 등 기본 검증이 필요하면 Bean Validation 추가.
해당 DTO가 “부분 수정” 용도라면 null 허용이 맞는지, 서비스에서 null 병합 규칙을 적용하는지 확인 부탁드립니다.
src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionCheck.java (1)
8-14: 필드 네이밍/의미 정합성 확인
classNumberGrade는 도메인getClassCode()와 의미가 달라 보입니다. API 일관성 측면에서classCode(또는 기존 공개 스키마와 동일 명)로 맞추는지 검토하세요.매핑 시
submission.getUser()가 LAZY 로딩이라면 트랜잭션 범위 내에서 호출되는지 확인 필요합니다.src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionAttachmentResponse.java (1)
7-14: 직렬화에서 null 필드 숨김 및 타입 중복 통합 제안
- URL 첨부의 파일 메타는 null 가능. 응답 가독성을 위해
@JsonInclude(Include.NON_NULL)권장.assignment쪽과submission쪽의FileType가 서로 다른 패키지(중복)로 보입니다. 공통 타입으로 통합 권장.import lombok.Builder; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; @Builder - public record SubmissionAttachmentResponse( +@JsonInclude(Include.NON_NULL) +public record SubmissionAttachmentResponse(src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentAttachmentDto.java (2)
6-12: 응답 네이밍/일관성 및 null 필드 처리
- 이 파일은 이름이
Dto인데, 제출 쪽은...Response. 한쪽으로 통일 권장.- 첨부 메타의 null 값 숨김을 위해
@JsonInclude(Include.NON_NULL)고려.
13-21: FileType 이중 정의 가능성
assignment.domain.FileType과submission.domain.FileType이 분리되어 있으면 API 소비자 입장에서 혼란. 공용 enum으로 통합하세요.src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionRepository.java (1)
20-20: 조회 패턴 최적화 고려
findAllByClassRoomAndUser가 빈번히 쓰이면(class_room_id, user_id)복합 인덱스 확인/추가 권장.src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/GetAllAssignmentDto.java (1)
12-13: 날짜 직렬화 포맷/타임존 일관성 확인 필요LocalDateTime에 패턴만 지정하면 타임존이 명시되지 않아 클라이언트/서버 TZ 설정에 따라 오해가 생길 수 있습니다. 시스템 전반(다른 DTO 포함)에서 동일 포맷과 TZ 전략(예: ISO-8601 + UTC, 또는 Asia/Seoul 고정)을 합의해 주세요. 전역 Jackson 설정으로 통일하거나 @jsonformat(timezone=...)을 명시하는 방식을 권장합니다.
src/main/java/hello/cluebackend/domain/submission/domain/Submission.java (5)
23-33: 연관관계 null 허용 여부 명시 및 무결성 보강Submission이 Assignment/User/ClassRoom 없이 존재할 수 없다면 DB 제약을 반영해야 합니다. 현재 @joincolumn에 nullable 속성이 없어 NULL 삽입이 가능해 보입니다.
권장 변경:
- @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "assignment_id") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignment_id", nullable = false) private Assignment assignment; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) private UserEntity user; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "class_room_id") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "class_room_id", nullable = false) private ClassRoom classRoom;또한 classRoom은 Assignment에도 존재할 가능성이 큽니다. 두 값 간 불일치가 생기지 않도록 서비스/도메인에서 동일성 검증을 추가하는 것을 권장합니다.
49-52: cancel 시 submittedAt 유지 여부 도메인 규칙 확인현재 cancel은 isSubmitted만 false로 바꾸고 submittedAt은 그대로 둡니다. “제출 취소 시 제출 시각 보존”이 의도된 규칙인지 확인 필요합니다. 일반적으로는 취소 시 제출시각을 null로 돌리거나 별도 이력으로 관리합니다.
54-58: 도메인에서 LocalDateTime.now() 직접 호출 지양테스트 용이성과 TZ 일관성 위해 Clock 주입 또는 상위 서비스에서 시각을 주입하는 패턴을 권장합니다.
15-15: @AllArgsConstructor 사용 재검토@AllArgsConstructor가 공개 생성자를 노출하여 JPA/Hibernate 및 빌더와 충돌/오용 여지가 있습니다. 제거하거나 접근 제어자를 PRIVATE/PROTECTED로 낮추세요.
-@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE)
60-63: Boolean 게터 명명 일관성필드명이 isSubmitted이면 Lombok이 isSubmitted() 게터를 생성합니다. 수동 getIsSubmitted()을 추가하면 혼란을 야기합니다. DTO/호출부를 isSubmitted()로 통일하고 본 메서드는 제거하는 것을 권장합니다.
- public boolean getIsSubmitted(){ - return this.isSubmitted; - } + // Lombok이 생성하는 isSubmitted() 게터 사용src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentResponseDto.java (2)
14-15: 날짜 직렬화 포맷 일관성GetAllAssignmentDto는 @jsonformat("yyyy-MM-dd HH:mm")을 사용하지만 여기에는 없음. 동일 포맷/전략으로 맞추거나 전역 설정으로 통일해 주세요.
권장 수정:
+import com.fasterxml.jackson.annotation.JsonFormat; @@ - LocalDateTime startDate, - LocalDateTime endDate, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate,
23-25: 람다 → 메서드 참조로 간단화가독성 개선을 위해 메서드 참조 사용을 권장합니다.
- List<AssignmentAttachmentDto> assignmentResponseDtos = assignmentAttachments.stream() - .map(aa -> AssignmentAttachmentDto.from(aa)) - .toList(); + List<AssignmentAttachmentDto> assignmentResponseDtos = assignmentAttachments.stream() + .map(AssignmentAttachmentDto::from) + .toList();src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionResponse.java (1)
12-15: 날짜 직렬화 포맷/타임존 통일다른 DTO와 동일하게 @jsonformat을 부여하거나 전역 Jackson 설정으로 맞추세요.
+import com.fasterxml.jackson.annotation.JsonFormat; @@ - LocalDateTime startDate, - LocalDateTime endDate, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate,src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java (2)
52-53: 람다 → 메서드 참조로 단순화사소하지만 일관된 스타일 유지 차원에서 권장합니다.
- .map(a -> GetAllAssignmentDto.from(a)) + .map(GetAllAssignmentDto::from)
61-70: 메서드명 오타 및 메시지 가독성
- findAssignmentAttachmentByIdOrderThrow → OrThrow 오타 수정 바랍니다.
- 예외 메시지 띄어쓰기: “찾을수” → “찾을 수”.
- public Assignment findByIdOrThrow(UUID assignmentId) { + public Assignment findByIdOrThrow(UUID assignmentId) { return assignmentRepository.findById(assignmentId) - .orElseThrow(() -> new EntityNotFoundException("해당 과제를 찾을수 없습니다.")); + .orElseThrow(() -> new EntityNotFoundException("해당 과제를 찾을 수 없습니다.")); } @@ - public AssignmentAttachment findAssignmentAttachmentByIdOrderThrow(UUID attachmentId){ + public AssignmentAttachment findAssignmentAttachmentByIdOrThrow(UUID attachmentId){ return assignmentAttachmentRepository.findById(attachmentId) - .orElseThrow(() -> new EntityNotFoundException("해당 첨부 파일을 찾을수 없습니다.")); + .orElseThrow(() -> new EntityNotFoundException("해당 첨부 파일을 찾을 수 없습니다.")); }메서드명 변경에 따른 호출부 전체 리네임이 필요합니다.
src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionQueryController.java (3)
45-53: 삭제 응답은 204 No Content가 더 적절합니다.본문 문자열 대신 상태 코드로 의도를 표현하는 것이 REST 관례에 부합합니다. 또한 이 API도 소유자/권한 검증이 필요합니다(서비스에
userId전달).@DeleteMapping("/{submissionAttachmentId}") public ResponseEntity<?> deleteSubmissionAttachment( @CurrentUser UUID userId, @PathVariable UUID submissionAttachmentId ){ - submissionQueryService.deleteSubmissionAttachment(submissionAttachmentId); - return ResponseEntity.ok("과제 첨부파일이 성공적으로 삭제되었습니다."); + submissionQueryService.deleteSubmissionAttachment(userId, submissionAttachmentId); + return ResponseEntity.noContent().build(); }
18-23: 클래스 명과 역할 불일치(QueryController가 명령 수행).제출/취소/첨부 추가/삭제 등은 커맨드 성격입니다. 기존
SubmissionCommandController와 역할을 정리하거나 클래스명을 Command로 조정해 API 표면을 일관화하세요.
21-21: 로거 미사용.
@Slf4j를 사용하지 않습니다. 유지하려면 의미있는 로그를 추가하거나 제거하세요.src/test/java/hello/cluebackend/domain/submission/presentation/SubmissionCommandControllerTest.java (4)
47-52:@WebMvcTest에서 별도MockMvcBuilders재구성은 필요 최소화 권장.커스텀 ArgumentResolver 주입 목적은 타당합니다만, 가능하면 테스트 슬라이스 설정으로 주입하는 쪽이 간결합니다(예:
@Import로 Config 등록). 유지 시에도 문제는 없습니다.
80-94: 단건 조회에서 200 + 빈 본문 계약 재확인 필요.리소스 부재 시 404 또는 204가 더 일반적입니다. 현재 계약이 의도된 것인지 명확화해 주세요. 의도대로라면 컨트롤러의 응답 처리도 일관되게 맞춰야 합니다.
테스트 대상 컨트롤러의 해당 엔드포인트 사양(설계 문서/기존 구현)을 확인해 주세요.
110-135: 다운로드 테스트에 내용 검증 추가 제안.헤더 검증은 좋습니다. 가능하면 응답 바이트 길이나 내용 일부 일치도 검증하면 회귀 방지에 유용합니다.
예:
andExpect(content().bytes(fileBytes))또는content().string(...)(텍스트인 경우).
59-66: 메서드 명/파라미터 의미 불일치 가능성.
findAllByAssignmentId(userId, classId)처럼 메서드명이 assignmentId인데 실제 인자는 classId입니다. 혼동을 줄이기 위해 시그니처/명명 정합성 확인을 권장합니다.src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentCommandController.java (1)
85-96: Content-Type 기본값 개선.
null일 때MediaType.ALL대신APPLICATION_OCTET_STREAM이 다운로드에 더 적합합니다.- MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.ALL; + MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.APPLICATION_OCTET_STREAM;src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java (2)
65-74: 중복 정적 팩토리(create와of)로 API 표면이 혼란스럽습니다.두 메서드가 동일 동작을 합니다. 하나로 통일하거나, 과도기에는 하나를
@Deprecated로 표시하세요.예시(과도기, create 유지):
- public static Assignment of(ClassRoom classRoom, UserEntity user, String title, String content, LocalDateTime startDate, LocalDateTime endDate) { - return Assignment.builder() - .classRoom(classRoom) - .user(user) - .title(title) - .content(content) - .startDate(startDate) - .endDate(endDate) - .build(); - } + @Deprecated // create(...)로 통일 예정 + public static Assignment of(ClassRoom classRoom, UserEntity user, String title, String content, LocalDateTime startDate, LocalDateTime endDate) { + return create(classRoom, user, title, content, startDate, endDate); + }
84-92: 연관관계 편의 메서드에 null 가드 추가 제안.NPE 및 무의미한 호출을 방지합니다.
public void addSubmission(Submission submission){ - submissions.add(submission); - submission.setAssignment(this); + if (submission == null) return; + submissions.add(submission); + submission.setAssignment(this); } public void removeSubmission(Submission submission){ - submissions.remove(submission); - submission.setAssignment(null); + if (submission == null) return; + submissions.remove(submission); + submission.setAssignment(null); }src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java (1)
33-42: 과제의 교실과 전달된 classroomId의 정합성 확인 필요.
users는classRoomId로 조회하고, 생성 시에는assignment.getClassRoom()을 사용합니다. 불일치 시 데이터 무결성 문제가 생길 수 있으므로 동일성 검증 또는 하나의 소스만 사용하세요.public void assignToAllStudentsInClassroom(UUID classroomId, Assignment assignment){ ClassRoom classRoom = classRoomService.findById(classroomId).toEntity(); + if (!assignment.getClassRoom().getClassRoomId().equals(classroomId)) { + throw new IllegalArgumentException("Assignment가 속한 교실과 요청 교실이 일치하지 않습니다."); + } List<UserEntity> users = classroomUserService.findAllClassroomUser(classRoom); List<Submission> submissions = users.stream() - .map(u -> new Submission(assignment, u,assignment.getClassRoom(), false, null)) + .map(u -> new Submission(assignment, u, classRoom, false, null)) .toList(); submissionRepository.saveAll(submissions); }src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionCommandController.java (3)
11-13: 와일드카드 import 최소화 제안DTO가 고정되어 있다면 명시적 import로 전환해 추후 리팩터링/IDE 탐색성을 높이는 편이 좋습니다.
29-39: 수업 구성원 검증 추가는 적절합니다만, 메서드/파라미터 네이밍 불일치
findAllByAssignmentId(userId, classId)호출은 실제로 "classId 기준 조회"입니다. 메서드/엔드포인트 이름과 주석을 클래스 기준으로 맞추면 혼동을 줄일 수 있습니다.
41-49: 단건 조회 메서드명 오해 소지 + 추가 접근제어 검토
findAllSubmission은 단건을 반환합니다.findSubmission등으로 정리 권장.- 단건 조회에서도 클래스 소속 검증을 고려하세요(현재 서비스 레벨 권한 체크에 의존). 최소한 교사/소유자 외 접근 차단이 서비스에서 확실히 이뤄지는지 확인 필요.
src/main/java/hello/cluebackend/domain/submission/application/SubmissionCommandService.java (2)
36-53: N+1 쿼리 발생 가능: 첨부파일 조회를 서브미션별로 반복
submissions스트림 내에서submissionAttachmentRepository.findAllBySubmission(...)를 호출하여 N+1이 발생합니다.findAllBySubmissionIn(List<Submission>)형태로 한 번에 조회 후submissionId기준으로 그룹핑하는 방식으로 줄이세요.
56-68: 컨트롤러 보완을 위한 서비스 보조 메서드 추가 제안컨트롤러 다운로드 엔드포인트에서 재사용할 권한 어서션을 서비스에 추가해 중복을 없애세요.
// SubmissionCommandService 내 신규 메서드 public void assertCanAccessAttachment(UUID userId, SubmissionAttachment sa) { UserEntity requester = userService.findById(userId).toEntity(); boolean isOwner = sa.getUser().getUserId().equals(userId); boolean isTeacher = requester.getRole() == Role.TEACHER; if (!isOwner && !isTeacher) { throw new AccessDeniedException("첨부파일에 접근 권한이 없습니다."); } }Also applies to: 80-104, 110-113
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
src/main/generated/hello/cluebackend/domain/assignment/domain/QAssignment.javais excluded by!**/generated/**src/main/generated/hello/cluebackend/domain/assignment/domain/QAssignmentAttachment.javais excluded by!**/generated/**src/main/generated/hello/cluebackend/domain/assignment/domain/QBaseEntity.javais excluded by!**/generated/**src/main/generated/hello/cluebackend/domain/submission/domain/QBaseEntity.javais excluded by!**/generated/**src/main/generated/hello/cluebackend/domain/submission/domain/QSubmission.javais excluded by!**/generated/**src/main/generated/hello/cluebackend/domain/submission/domain/QSubmissionAttachment.javais excluded by!**/generated/**
📒 Files selected for processing (42)
src/main/java/hello/cluebackend/ClueBackendApplication.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/request/AssignmentAttachmentDto.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentResponseDto.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/GetAllAssignmentDto.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/api/dto/response/SubmissionCheck.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java(3 hunks)src/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.java(2 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java(2 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/domain/BaseEntity.java(0 hunks)src/main/java/hello/cluebackend/domain/assignment/persistence/AssignmentRepository.java(2 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentCommandController.java(2 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentQueryController.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/AssignmentAttachmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/CreateAssignmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/ModifyAssignmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentAttachmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentResponseDto.java(1 hunks)src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/GetAllAssignmentDto.java(1 hunks)src/main/java/hello/cluebackend/domain/classroom/domain/ClassRoom.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAssignmentAttachmentDto.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAttachmentUrlDto.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionAttachmentDto.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionDto.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/application/SubmissionCommandService.java(2 hunks)src/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java(6 hunks)src/main/java/hello/cluebackend/domain/submission/domain/BaseEntity.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/domain/FileType.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/domain/Submission.java(3 hunks)src/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionAttachmentRepository.java(0 hunks)src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionRepository.java(2 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionCommandController.java(3 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/SubmissionQueryController.java(3 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/dto/request/SubmissionAttachmentUrlDto.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionAttachmentResponse.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionCheck.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionDto.java(1 hunks)src/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionResponse.java(1 hunks)src/main/resources/application-dev.yml(1 hunks)src/test/java/hello/cluebackend/domain/submission/presentation/SubmissionCommandControllerTest.java(1 hunks)
💤 Files with no reviewable changes (12)
- src/main/java/hello/cluebackend/domain/submission/persistence/SubmissionAttachmentRepository.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/SubmissionCheck.java
- src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionDto.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/GetAllAssignmentDto.java
- src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAttachmentUrlDto.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/request/AssignmentAttachmentDto.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentAttachmentDto.java
- src/main/java/hello/cluebackend/domain/assignment/api/dto/response/AssignmentResponseDto.java
- src/main/java/hello/cluebackend/domain/assignment/domain/BaseEntity.java
- src/main/java/hello/cluebackend/domain/submission/api/dto/request/SubmissionAssignmentAttachmentDto.java
- src/main/java/hello/cluebackend/domain/submission/domain/BaseEntity.java
- src/main/java/hello/cluebackend/domain/submission/api/dto/response/SubmissionAttachmentDto.java
🧰 Additional context used
🧠 Learnings (2)
📚 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/submission/presentation/dto/request/SubmissionAttachmentUrlDto.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentResponseDto.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/AssignmentAttachmentDto.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/AssignmentAttachmentDto.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/AssignmentQueryController.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/ModifyAssignmentDto.javasrc/main/java/hello/cluebackend/domain/assignment/application/AssignmentQueryService.javasrc/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/CreateAssignmentDto.javasrc/main/java/hello/cluebackend/domain/submission/presentation/SubmissionQueryController.javasrc/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.javasrc/main/java/hello/cluebackend/domain/submission/presentation/dto/response/SubmissionAttachmentResponse.javasrc/main/java/hello/cluebackend/domain/submission/application/SubmissionQueryService.java
📚 Learning: 2025-07-08T23:38:29.680Z
Learnt from: fixgramwork
PR: EscapeFrame/clueBackend#23
File: src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java:17-23
Timestamp: 2025-07-08T23:38:29.680Z
Learning: This project uses OAuth2 authentication and does not require traditional password storage or validation. The RegisterUserService in src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java should not store passwords since OAuth2 handles authentication externally.
Applied to files:
src/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java
🔇 Additional comments (6)
src/main/java/hello/cluebackend/ClueBackendApplication.java (1)
19-22: 감사 로그 주체(Auditor) 빈 주석 처리 — 사용 여부 확인 필요자동 검사 스크립트가 파일을 검색하지 못했습니다. 리포지토리에서 @createdby, @LastModifiedBy, @CreatedDate, @LastModifiedDate, @EnableJpaAuditing, AuditorAware, BaseEntity 사용 여부를 확인하세요. 한 곳이라도 있으면 AuditorAware 빈이 필요합니다.
옵션 A) 감사 기능 미사용 시:
- @EnableJpaAuditing(현재 Line 13) 제거하고 주석 처리된 AuditorAware 블록(19–22) 삭제해 설정 일관성 유지.
옵션 B) 감사 기능 사용 시:
- 현재 주석된 구현은 매 호출마다 랜덤 UUID를 반환해 감사가 무의미합니다. SecurityContext 기반으로 교체하세요.
예시(diff: 19–22 대체)
-// @Bean -// public AuditorAware<String> auditorProvider(){ -// return () -> Optional.of(UUID.randomUUID().toString()); -// } baseEntity의 필요성을 느끼지 못해서 잠시 정지 + @Bean + public AuditorAware<String> auditorProvider() { + return () -> java.util.Optional.ofNullable( + org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication() + ) + .filter(org.springframework.security.core.Authentication::isAuthenticated) + .map(org.springframework.security.core.Authentication::getName); + }추가 import: org.springframework.security.core.Authentication, org.springframework.security.core.context.SecurityContextHolder
로컬에서 재확인용(예시):
git grep -n --untracked -e '@CreatedBy' -e '@LastModifiedBy' -e '@CreatedDate' -e '@LastModifiedDate' -e 'AuditorAware' -e '@EnableJpaAuditing' -e 'BaseEntity' || true # 또는 grep -nR --include='*.java' -e '@CreatedBy' -e '@LastModifiedBy' -e '@CreatedDate' -e '@LastModifiedDate' -e 'AuditorAware' -e '@EnableJpaAuditing' -e 'BaseEntity' .src/main/java/hello/cluebackend/domain/classroom/domain/ClassRoom.java (1)
23-25: GenerationType.UUID 사용 시 DB 매핑 및 Dialect/버전 확인 필요엔티티들이 GenerationType.UUID를 다수 사용하고 있고 class_room_id를 참조하는 FK도 존재합니다. application.yml에서 dialect/hibernate 항목은 검색되지 않았습니다. 아래 확인 및 조치 필요.
발견된 파일 (GenerationType.UUID 사용):
- src/main/java/hello/cluebackend/domain/classroom/domain/ClassRoom.java
- src/main/java/hello/cluebackend/domain/submission/domain/Submission.java
- src/main/java/hello/cluebackend/domain/document/domain/Document.java
- src/main/java/hello/cluebackend/domain/directory/domain/Directory.java
- src/main/java/hello/cluebackend/domain/classroomuser/domain/ClassRoomUser.java
- src/main/java/hello/cluebackend/domain/assignment/domain/Assignment.java
- src/main/java/hello/cluebackend/domain/assignment/domain/AssignmentAttachment.java
- src/main/java/hello/cluebackend/domain/submission/domain/SubmissionAttachment.java
- src/main/java/hello/cluebackend/domain/user/domain/UserEntity.java
class_room_id를 참조하는 위치 (FK 후보):
- src/main/java/hello/cluebackend/domain/submission/domain/Submission.java
- src/main/java/hello/cluebackend/domain/document/domain/Document.java
- src/main/java/hello/cluebackend/domain/classroomuser/domain/ClassRoomUser.java
- src/main/java/hello/cluebackend/domain/directory/domain/Directory.java
권장 조치 (간단):
- DB 컬럼 타입(CHAR(36) 또는 BINARY(16))과 엔티티 매핑 일치 여부 확인; 필요 시 @column(columnDefinition=...) 또는 적절한 Type 매핑 추가.
- 애플리케이션 설정에서 사용 중인 Hibernate 버전·Dialect 확인(GenerationType.UUID 지원 여부). 지원하지 않으면 명시적 UUID generator(org.hibernate.id.UUIDGenerator 등) 또는 다른 전략 사용 검토.
- 스키마(DDL) 생성/마이그레이션에서 UUID 컬럼이 기대한 타입으로 생성되는지 검증.
참고 원본 스니펫:
@Id @GeneratedValue(strategy = GenerationType.UUID) @Column(name = "class_room_id", nullable = false, updatable = false) private UUID classRoomId;src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/CreateAssignmentDto.java (1)
11-18: DTO 필드 제약 보강 — 제목/내용 길이·마감일·기간 유효성 추가NotNull만으로는 품질 보장이 어렵습니다. 제목/내용 길이 제한과 마감일의 미래 제약을 추가하고, 필요 시 시작/종료 순서(endDate > startDate) 클래스 레벨 검증을 적용하세요.
파일: src/main/java/hello/cluebackend/domain/assignment/presentation/dto/request/CreateAssignmentDto.java (11-18행)
@Builder public record CreateAssignmentDto ( @NotNull @JsonProperty("class_id") UUID 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 @jakarta.validation.constraints.Size(min = 1, max = 100) String title, + @NotNull @jakarta.validation.constraints.Size(min = 1, max = 5000) String content, + @NotNull @JsonProperty("start_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime startDate, + @NotNull @jakarta.validation.constraints.Future @JsonProperty("end_date") @JsonFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime endDate ){}서비스/도메인에서 endDate > startDate(기간 유효성) 검증이 이미 수행되는지 확인 필요.
src/main/java/hello/cluebackend/domain/assignment/presentation/dto/response/GetAllAssignmentDto.java (1)
15-22: 정적 팩토리 매핑 깔끔합니다도메인 -> DTO 필드 매핑이 명확하고 불변 레코드와 잘 맞습니다.
src/main/java/hello/cluebackend/domain/assignment/application/AssignmentCommandService.java (1)
36-36: DTO 정적 팩토리 적용 적절중복 매핑 제거되고 가독성 좋아졌습니다.
src/main/java/hello/cluebackend/domain/submission/application/SubmissionCommandService.java (1)
110-113: 예외 메시지/타입 적절, 메서드 명확 — LGTM엔티티 미존재 시 JPA 표준 예외를 던지는 방식은 합리적입니다.
Summary by CodeRabbit