Skip to content

[NO-ISSUE] 공지사항 응답 구조 변경#207

Merged
Develop-KIM merged 2 commits intodevelopfrom
refactor/NO-ISSUE-author
Mar 2, 2026
Merged

[NO-ISSUE] 공지사항 응답 구조 변경#207
Develop-KIM merged 2 commits intodevelopfrom
refactor/NO-ISSUE-author

Conversation

@Develop-KIM
Copy link
Member

@Develop-KIM Develop-KIM commented Mar 2, 2026

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • SVG 파일 업로드 지원 추가 (기존 JPG, PNG, GIF, WebP 외)
    • 공지사항 응답에 작성자 정보(이름, ID) 포함
  • 개선 사항

    • 데이터 조회 성능 최적화를 통한 더 빠른 응답 시간

Copilot AI review requested due to automatic review settings March 2, 2026 12:51
@Develop-KIM Develop-KIM added the refactoring 외부 동작은 유지하되 코드 구조를 개선하거나 유지보수성을 높이기 위한 내부 변경에 사용합니다. label Mar 2, 2026
@Develop-KIM Develop-KIM merged commit 783f5ed into develop Mar 2, 2026
2 checks passed
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9404643 and b10ce91.

📒 Files selected for processing (11)
  • src/main/java/com/souzip/adapter/storage/file/NcpStorage.java
  • src/main/java/com/souzip/application/notice/NoticeQueryService.java
  • src/main/java/com/souzip/application/notice/assembler/NoticeResponseAssembler.java
  • src/main/java/com/souzip/application/notice/dto/NoticeAuthorResponse.java
  • src/main/java/com/souzip/application/notice/dto/NoticeResponse.java
  • src/main/java/com/souzip/domain/admin/infrastructure/persistence/AdminJpaRepository.java
  • src/main/java/com/souzip/domain/admin/infrastructure/persistence/AdminRepositoryImpl.java
  • src/main/java/com/souzip/domain/admin/repository/AdminRepository.java
  • src/test/java/com/souzip/adapter/webapi/admin/AdminNoticeApiTest.java
  • src/test/java/com/souzip/adapter/webapi/user/NoticeApiTest.java
  • src/test/java/com/souzip/application/notice/NoticeQueryServiceTest.java

Walkthrough

Notice 조회 서비스의 응답 구성 로직을 새로운 NoticeResponseAssembler 컴포넌트로 추출하고, Notice 응답 DTO의 authorId 필드를 NoticeAuthorResponse 객체로 변경하여 작성자 정보를 구조화했습니다. Admin 저장소에 배치 조회 메서드를 추가했고 SVG 파일 형식을 지원하도록 확장했습니다.

Changes

Cohort / File(s) Summary
SVG 파일 형식 지원
src/main/java/com/souzip/adapter/storage/file/NcpStorage.java
ALLOWED_EXTENSIONS에 ".svg" 파일 확장자 추가하여 SVG 이미지 업로드 지원
Notice 응답 DTO 구조 개선
src/main/java/com/souzip/application/notice/dto/NoticeResponse.java, src/main/java/com/souzip/application/notice/dto/NoticeAuthorResponse.java
authorId(UUID) 필드를 author(NoticeAuthorResponse) 객체로 변경하여 작성자 정보를 구조화; 새로운 NoticeAuthorResponse 레코드 추가
Notice 조회 서비스 리팩토링
src/main/java/com/souzip/application/notice/NoticeQueryService.java, src/main/java/com/souzip/application/notice/assembler/NoticeResponseAssembler.java
응답 구성 로직을 새로운 NoticeResponseAssembler 컴포넌트로 추출; FileFinder 의존성을 NoticeResponseAssembler로 교체하여 관심사 분리
Admin 저장소 확장
src/main/java/com/souzip/domain/admin/repository/AdminRepository.java, src/main/java/com/souzip/domain/admin/infrastructure/persistence/AdminRepositoryImpl.java, src/main/java/com/souzip/domain/admin/infrastructure/persistence/AdminJpaRepository.java
AdminRepository에 findAllByIds 메서드 추가하여 여러 ID로 Admin 엔티티 배치 조회 지원
API 레이어 테스트 업데이트
src/test/java/com/souzip/adapter/webapi/admin/AdminNoticeApiTest.java, src/test/java/com/souzip/adapter/webapi/user/NoticeApiTest.java
NoticeResponse 구조 변경에 따른 테스트 데이터 및 검증 로직 업데이트; author 객체 사용으로 API 문서화 스펙 변경
서비스 레이어 테스트 업데이트
src/test/java/com/souzip/application/notice/NoticeQueryServiceTest.java
FileFinder 모킹을 NoticeResponseAssembler 모킹으로 교체; assembler 위임 로직 검증으로 변경

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NoticeQueryService
    participant NoticeResponseAssembler
    participant FileFinder
    participant AdminRepository
    participant Database

    Client->>NoticeQueryService: findByIdWithFiles(noticeId)
    NoticeQueryService->>NoticeResponseAssembler: assemble(notice)
    
    NoticeResponseAssembler->>FileFinder: findFilesBy(NOTICE, noticeId)
    FileFinder->>Database: query files
    Database-->>FileFinder: List<FileResponse>
    FileFinder-->>NoticeResponseAssembler: files list
    
    NoticeResponseAssembler->>AdminRepository: findAllByIds([authorId])
    AdminRepository->>Database: query admin by id
    Database-->>AdminRepository: List<Admin>
    AdminRepository-->>NoticeResponseAssembler: Admin objects
    
    NoticeResponseAssembler->>NoticeResponseAssembler: toAuthorResponse(admin)
    NoticeResponseAssembler->>NoticeResponseAssembler: toResponse(notice, filesMap, authorMap)
    NoticeResponseAssembler-->>NoticeQueryService: NoticeResponse
    
    NoticeQueryService-->>Client: NoticeResponse with nested author
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #205: Notice 관련 클래스(NoticeResponseAssembler, NoticeAuthorResponse, NoticeQueryService, NoticeResponse)의 동일한 영역에 대한 리팩토링 및 추가 작업으로 직접 관련
  • #151: AdminRepository 레이어에 새로운 배치 조회 메서드 추가로 코드 수준에서 관련

Suggested labels

test

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/NO-ISSUE-author

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b10ce91804

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private final ObjectStorageProperties properties;

private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp");
private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg");

Choose a reason for hiding this comment

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

P1 Badge SVG 업로드 허용 전에 active content 방어를 추가하세요

이번 변경으로 .svg가 허용되었는데, 현재 업로드 경로는 NcpStorage#createMetadata에서 file.getContentType()를 그대로 저장해 사용자 제공 콘텐츠를 그대로 브라우저에 노출합니다. SVG는 스크립트/foreignObject를 포함할 수 있는 active content라서, 업로드된 URL을 직접 열거나 임베드하는 환경에서 stored XSS가 발생할 수 있으므로(특히 관리자/운영자가 파일을 미리보기하는 경우) SVG를 계속 허용하려면 sanitize/변환 처리 또는 강제 다운로드 정책 같은 방어가 필요합니다.

Useful? React with 👍 / 👎.

) {
List<FileResponse> files = filesMap.getOrDefault(notice.getId(), List.of());

NoticeAuthorResponse author = authorMap.get(notice.getAuthorId());

Choose a reason for hiding this comment

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

P2 Badge 작성자 조회 실패 시 author를 null로 내려주지 마세요

NoticeResponseAssembler#toResponseauthorMap.get(notice.getAuthorId()) 결과를 그대로 사용해, admin 조회에서 누락되면 author가 null로 직렬화됩니다. 이 코드베이스는 notice.author_id에 FK가 없고(V13__create_notice_table.sql), admin 삭제가 가능해(AdminManagementService#deleteAdmin) orphan notice가 현실적으로 생길 수 있으므로, 기존에는 항상 전달되던 작성자 식별 정보가 사라져 클라이언트의 author.authorId 접근이 깨집니다; 조회 실패 시 fallback author를 채우거나 명시적 예외로 계약을 유지해야 합니다.

Useful? React with 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR changes the notice (공지사항) response structure to replace the flat authorId: UUID field with a nested author: { authorId, username } object. A new NoticeResponseAssembler component is introduced to encapsulate the logic for fetching both files and author details (via AdminRepository) and composing them into NoticeResponse DTOs. NoticeQueryService is simplified to delegate all assembly to this new component. Additionally, .svg is added to the list of allowed file upload extensions.

Changes:

  • Introduced NoticeAuthorResponse DTO and replaced authorId flat field in NoticeResponse with the new nested author object.
  • Introduced NoticeResponseAssembler to batch-fetch files and authors and compose NoticeResponse; refactored NoticeQueryService to delegate to it. Added findAllByIds to AdminRepository/AdminRepositoryImpl/AdminJpaRepository to support batch author lookups.
  • Updated all tests (NoticeQueryServiceTest, NoticeApiTest, AdminNoticeApiTest) and REST Docs to reflect the new response structure; added .svg to allowed upload extensions in NcpStorage.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
NoticeResponse.java Replaced flat UUID authorId with nested NoticeAuthorResponse author; updated from() factory method signature
NoticeAuthorResponse.java New DTO record holding author UUID and username
NoticeResponseAssembler.java New component encapsulating batch file + author fetching and response assembly
NoticeQueryService.java Simplified; delegates all assembly to NoticeResponseAssembler
AdminRepository.java / AdminRepositoryImpl.java / AdminJpaRepository.java Added findAllByIds for batch admin lookup
NoticeQueryServiceTest.java Updated mocks and assertions to reflect assembler delegation
NoticeApiTest.java / AdminNoticeApiTest.java Updated REST Docs and assertions for new author response shape
NcpStorage.java Added .svg to allowed upload extensions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +31
Map<Long, List<FileResponse>> filesMap = fetchFilesMap(List.of(notice));
Map<UUID, NoticeAuthorResponse> authorMap = fetchAuthorMap(List.of(notice));

return toResponse(notice, filesMap, authorMap);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The assemble(Notice notice) method creates single-element lists (List.of(notice)) just to call fetchFilesMap and fetchAuthorMap, which were designed for batching over a list of notices. This means for every single-notice lookup, there is still a separate DB query for files and a separate DB query for the author, even though the batching could be shortcut. More importantly, this is slightly wasteful compared to having single-entity variants of the fetch methods. This is a minor design concern but worth noting for future optimization.

Suggested change
Map<Long, List<FileResponse>> filesMap = fetchFilesMap(List.of(notice));
Map<UUID, NoticeAuthorResponse> authorMap = fetchAuthorMap(List.of(notice));
return toResponse(notice, filesMap, authorMap);
Map<Long, List<FileResponse>> filesByEntityId =
fileFinder.findFilesByEntityIds(EntityType.NOTICE, List.of(notice.getId()));
List<FileResponse> files = filesByEntityId.getOrDefault(notice.getId(), List.of());
NoticeAuthorResponse author = adminRepository.findAllByIds(List.of(notice.getAuthorId()))
.stream()
.findFirst()
.map(this::toAuthorResponse)
.orElse(null);
return NoticeResponse.from(notice, author, files);

Copilot uses AI. Check for mistakes.
Comment on lines +349 to +350
NoticeAuthorResponse author1 = NoticeAuthorResponse.of(TEST_ADMIN_ID, "admin");
NoticeAuthorResponse author2 = NoticeAuthorResponse.of(TEST_ADMIN_ID, "admin");
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The two variables author1 and author2 are created separately but are identical (both are NoticeAuthorResponse.of(TEST_ADMIN_ID, "admin")). Using a single author variable (as done in all the other test methods in this PR) would be cleaner and less confusing for readers.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +88
public class NoticeResponseAssembler {

private final FileFinder fileFinder;
private final AdminRepository adminRepository;

public NoticeResponse assemble(Notice notice) {
Map<Long, List<FileResponse>> filesMap = fetchFilesMap(List.of(notice));
Map<UUID, NoticeAuthorResponse> authorMap = fetchAuthorMap(List.of(notice));

return toResponse(notice, filesMap, authorMap);
}

public List<NoticeResponse> assembleAll(List<Notice> notices) {
if (notices.isEmpty()) {
return List.of();
}

Map<Long, List<FileResponse>> filesMap = fetchFilesMap(notices);
Map<UUID, NoticeAuthorResponse> authorMap = fetchAuthorMap(notices);

return notices.stream()
.map(n -> toResponse(n, filesMap, authorMap))
.toList();
}

private NoticeResponse toResponse(
Notice notice,
Map<Long, List<FileResponse>> filesMap,
Map<UUID, NoticeAuthorResponse> authorMap
) {
List<FileResponse> files = filesMap.getOrDefault(notice.getId(), List.of());

NoticeAuthorResponse author = authorMap.get(notice.getAuthorId());

return NoticeResponse.from(notice, author, files);
}

private Map<Long, List<FileResponse>> fetchFilesMap(List<Notice> notices) {
List<Long> ids = notices.stream().map(Notice::getId).toList();
if (ids.isEmpty()) {
return Collections.emptyMap();
}
return fileFinder.findFilesByEntityIds(EntityType.NOTICE, ids);
}

private Map<UUID, NoticeAuthorResponse> fetchAuthorMap(List<Notice> notices) {
List<UUID> authorIds = notices.stream()
.map(Notice::getAuthorId)
.distinct()
.toList();

if (authorIds.isEmpty()) {
return Collections.emptyMap();
}

return adminRepository.findAllByIds(authorIds).stream()
.map(this::toAuthorResponse)
.collect(Collectors.toMap(NoticeAuthorResponse::authorId, Function.identity()));
}

private NoticeAuthorResponse toAuthorResponse(Admin admin) {
return NoticeAuthorResponse.of(
admin.getId(),
admin.getUsername().value()
);
}
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

There is no test class for NoticeResponseAssembler, which is new, non-trivial logic containing file fetching, author resolution, and combining of results. Similar application-layer services in the codebase (e.g., FileQueryServiceTest, FileModifyServiceTest, NoticeModifyServiceTest) all have dedicated unit tests. The assembler's behavior — including the null author case, the empty list short-circuit path in assembleAll, and the batching logic in fetchAuthorMap — should be covered by dedicated unit tests.

Copilot uses AI. Check for mistakes.
import com.souzip.application.notice.provided.NoticeFinder;
import com.souzip.application.notice.provided.NoticeRegister;
import com.souzip.docs.RestDocsSupport;
import com.souzip.domain.admin.model.AdminRole;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The AdminRole import is unused in this file. No usage of AdminRole was found anywhere in AdminNoticeApiTest.java. This import was added in this PR but is never referenced in the test code.

Suggested change
import com.souzip.domain.admin.model.AdminRole;

Copilot uses AI. Check for mistakes.
) {
List<FileResponse> files = filesMap.getOrDefault(notice.getId(), List.of());

NoticeAuthorResponse author = authorMap.get(notice.getAuthorId());
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

In toResponse(), authorMap.get(notice.getAuthorId()) returns null when the author is not found in the map (e.g., if an admin was deleted after writing the notice, or if the findAllByIds query returns incomplete results). This null is then passed directly into NoticeResponse.from(notice, author, files), resulting in a NoticeResponse with a null author field. When this response is serialized or accessed (e.g., response.author().authorId()), it will cause a NullPointerException or produce null in the JSON output, which can break clients expecting the author object.

Consider using authorMap.getOrDefault(notice.getAuthorId(), ...) with a fallback value, or throwing a descriptive exception when the author cannot be resolved.

Suggested change
NoticeAuthorResponse author = authorMap.get(notice.getAuthorId());
NoticeAuthorResponse author = authorMap.get(notice.getAuthorId());
if (author == null) {
throw new IllegalStateException(
"Author not found for notice. noticeId=" + notice.getId()
+ ", authorId=" + notice.getAuthorId()
);
}

Copilot uses AI. Check for mistakes.
@Develop-KIM Develop-KIM deleted the refactor/NO-ISSUE-author branch March 4, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactoring 외부 동작은 유지하되 코드 구조를 개선하거나 유지보수성을 높이기 위한 내부 변경에 사용합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants