From ace16083cb3da4c6ce99e21a821e175d06846e91 Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Sat, 10 Oct 2026 00:18:06 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=C2=B7=EC=88=98=EC=A0=95=C2=B7=EC=82=AD=EC=A0=9C=C2=B7?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정은 요청의 수정 시각이 저장된 값과 다르면 NOTICE_UPDATE_CONFLICT로 거부한다. Notice에 수정 시각을 노출하고, 삭제는 소프트 삭제로 처리한다. --- .../welfare/domain/notice/domain/Notice.java | 34 ++++++++++++++- .../notice/domain/NoticeCreateCommand.java | 13 ++++++ .../domain/notice/domain/NoticeErrorCode.java | 3 +- .../notice/domain/NoticeUpdateCommand.java | 17 ++++++++ .../notice/repository/NoticeRepository.java | 7 +++ .../domain/notice/service/NoticeService.java | 12 ++++++ .../service/impl/NoticeServiceImpl.java | 43 +++++++++++++++++++ .../stream/db/welfare/NoticeJpaEntity.java | 3 +- .../db/welfare/NoticeRepositoryImpl.java | 15 +++++++ 9 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java create mode 100644 core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeUpdateCommand.java diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java index cfbbd393..f8923553 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java @@ -21,6 +21,7 @@ public class Notice { private List attachmentIds; private List imageIds; private LocalDateTime createdAt; + private LocalDateTime updatedAt; public static Notice of( Long id, @@ -31,8 +32,37 @@ public static Notice of( Long createdBy, List attachmentIds, List imageIds, - LocalDateTime createdAt + LocalDateTime createdAt, + LocalDateTime updatedAt ) { - return new Notice(id, title, content, category, pinned, createdBy, attachmentIds, imageIds, createdAt); + return new Notice(id, title, content, category, pinned, createdBy, attachmentIds, imageIds, createdAt, updatedAt); + } + + // 새 공지는 고정되지 않은 채로 등록한다. 생성·수정 시각은 DB가 채운다 + public static Notice create(NoticeCreateCommand command) { + return new Notice( + null, + command.title(), + command.content(), + command.category(), + false, + command.createdBy(), + command.attachmentIds(), + command.imageIds(), + null, + null + ); + } + + public void update(NoticeUpdateCommand command) { + this.title = command.title(); + this.content = command.content(); + this.category = command.category(); + this.attachmentIds = command.attachmentIds(); + this.imageIds = command.imageIds(); + } + + public void changePinned(boolean pinned) { + this.pinned = pinned; } } diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java new file mode 100644 index 00000000..0985a56c --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java @@ -0,0 +1,13 @@ +package kr.ac.kookmin.stream.welfare.domain.notice.domain; + +import java.util.List; + +public record NoticeCreateCommand( + String title, + String content, + NoticeCategory category, + List imageIds, + List attachmentIds, + Long createdBy +) { +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeErrorCode.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeErrorCode.java index dd50ba77..86321490 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeErrorCode.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeErrorCode.java @@ -12,7 +12,8 @@ public enum NoticeErrorCode implements ErrorCode { NOTICE_NOT_FOUND(ErrorStatus.NOT_FOUND, "공지를 찾을 수 없습니다."), - NOTICE_INVALID_CURSOR(ErrorStatus.BAD_REQUEST, "유효하지 않은 커서입니다."); + NOTICE_INVALID_CURSOR(ErrorStatus.BAD_REQUEST, "유효하지 않은 커서입니다."), + NOTICE_UPDATE_CONFLICT(ErrorStatus.CONFLICT, "다른 관리자가 먼저 공지를 수정했습니다. 새로고침한 뒤 다시 수정해 주세요."); private final int status; private final String message; diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeUpdateCommand.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeUpdateCommand.java new file mode 100644 index 00000000..7082ae32 --- /dev/null +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeUpdateCommand.java @@ -0,0 +1,17 @@ +package kr.ac.kookmin.stream.welfare.domain.notice.domain; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @param expectedUpdatedAt 수정 화면이 공지를 읽었을 때의 수정 시각. 그 사이 다른 관리자가 고쳤는지 가려내는 데 쓴다 + */ +public record NoticeUpdateCommand( + String title, + String content, + NoticeCategory category, + List imageIds, + List attachmentIds, + LocalDateTime expectedUpdatedAt +) { +} diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/repository/NoticeRepository.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/repository/NoticeRepository.java index 5e9b101f..1697d3ed 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/repository/NoticeRepository.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/repository/NoticeRepository.java @@ -9,4 +9,11 @@ public interface NoticeRepository { CursorSliceResult findAll(NoticeCategory category, NoticeCursor cursor, int size); Optional findById(Long id); + Notice save(Notice notice); + + /** + * 공지를 소프트 삭제한다. 이미 삭제됐거나 없는 공지면 false를 돌려준다. + * 변경은 호출한 쪽 트랜잭션이 커밋될 때 반영된다. + */ + boolean delete(Long id); } diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java index 881d912e..8b678c64 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java @@ -3,9 +3,21 @@ import kr.ac.kookmin.stream.common.CursorSliceResult; import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCreateCommand; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCursor; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeUpdateCommand; public interface NoticeService { CursorSliceResult getNotices(NoticeCategory category, NoticeCursor cursor, int size); Notice getNotice(Long id); + Notice create(NoticeCreateCommand command); + + /** + * 공지를 수정한다. 명령의 수정 시각이 저장된 값과 다르면(그 사이 다른 관리자가 고친 경우) + * {@code NOTICE_UPDATE_CONFLICT}로 거부한다. + */ + Notice update(Long id, NoticeUpdateCommand command); + + void delete(Long id); + void changePinned(Long id, boolean pinned); } diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java index 21ed9081..c8094464 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java @@ -1,11 +1,15 @@ package kr.ac.kookmin.stream.welfare.domain.notice.service.impl; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import kr.ac.kookmin.stream.common.BusinessException; import kr.ac.kookmin.stream.common.CursorSliceResult; import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCreateCommand; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCursor; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeErrorCode; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeUpdateCommand; import kr.ac.kookmin.stream.welfare.domain.notice.repository.NoticeRepository; import kr.ac.kookmin.stream.welfare.domain.notice.service.NoticeService; import lombok.RequiredArgsConstructor; @@ -34,4 +38,43 @@ public Notice getNotice(Long id) { return noticeRepository.findById(id) .orElseThrow(() -> new BusinessException(NoticeErrorCode.NOTICE_NOT_FOUND)); } + + @Override + public Notice create(NoticeCreateCommand command) { + return noticeRepository.save(Notice.create(command)); + } + + @Override + @Transactional + public Notice update(Long id, NoticeUpdateCommand command) { + Notice notice = getNotice(id); + validateNotModified(notice, command.expectedUpdatedAt()); + + notice.update(command); + return noticeRepository.save(notice); + } + + @Override + @Transactional + public void delete(Long id) { + if (!noticeRepository.delete(id)) { + throw new BusinessException(NoticeErrorCode.NOTICE_NOT_FOUND); + } + } + + @Override + @Transactional + public void changePinned(Long id, boolean pinned) { + Notice notice = getNotice(id); + notice.changePinned(pinned); + noticeRepository.save(notice); + } + + // updated_at이 초 단위 DATETIME이라 같은 초로 맞춰 비교한다. 수정 화면이 읽은 뒤 고쳐졌다면 시각이 달라진다 + private void validateNotModified(Notice notice, LocalDateTime expectedUpdatedAt) { + LocalDateTime current = notice.getUpdatedAt().truncatedTo(ChronoUnit.SECONDS); + if (!current.equals(expectedUpdatedAt.truncatedTo(ChronoUnit.SECONDS))) { + throw new BusinessException(NoticeErrorCode.NOTICE_UPDATE_CONFLICT); + } + } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java index 7dcb50d2..3def9584 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeJpaEntity.java @@ -73,6 +73,7 @@ public static NoticeJpaEntity from(Notice notice) { } public Notice toDomain() { - return Notice.of(id, title, content, category, pinned, createdBy, attachmentIds, imageIds, getCreatedAt()); + return Notice.of( + id, title, content, category, pinned, createdBy, attachmentIds, imageIds, getCreatedAt(), getUpdatedAt()); } } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeRepositoryImpl.java index d208b73f..aba0e249 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeRepositoryImpl.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/NoticeRepositoryImpl.java @@ -33,4 +33,19 @@ public CursorSliceResult findAll(NoticeCategory category, NoticeCursor c public Optional findById(Long id) { return noticeJpaRepository.findByIdAndIsDeletedFalse(id).map(NoticeJpaEntity::toDomain); } + + @Override + public Notice save(Notice notice) { + return noticeJpaRepository.save(NoticeJpaEntity.from(notice)).toDomain(); + } + + @Override + public boolean delete(Long id) { + return noticeJpaRepository.findByIdAndIsDeletedFalse(id) + .map(entity -> { + entity.delete(); + return true; + }) + .orElse(false); + } } From 6164589662ef5fc7947e9cfced3793b045d87d9b Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Sat, 10 Oct 2026 00:18:07 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin-api/build.gradle.kts | 1 + .../admin/welfare/notice/AdminNoticeApi.java | 54 ++++++++++++++ .../welfare/notice/AdminNoticeController.java | 70 +++++++++++++++++++ .../notice/request/NoticeCreateRequest.java | 49 +++++++++++++ .../notice/request/NoticePinRequest.java | 11 +++ .../notice/request/NoticeUpdateRequest.java | 56 +++++++++++++++ .../notice/response/NoticeCreateResponse.java | 10 +++ .../notice/response/NoticeUpdateResponse.java | 10 +++ .../usecase/AdminNoticeWriteUseCase.java | 48 +++++++++++++ 9 files changed, 309 insertions(+) create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeApi.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticePinRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeCreateResponse.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeUpdateResponse.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java diff --git a/api/admin-api/build.gradle.kts b/api/admin-api/build.gradle.kts index bd5f43b3..983679fe 100644 --- a/api/admin-api/build.gradle.kts +++ b/api/admin-api/build.gradle.kts @@ -7,6 +7,7 @@ description = "운영진 콘솔 — ADMIN, /v1/admin/**" dependencies { implementation(project(":api:common-api")) implementation(project(":core:common")) + implementation(project(":core:domain:file")) implementation(project(":core:domain:internal")) implementation(project(":core:domain:member")) implementation(project(":core:domain:welfare")) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeApi.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeApi.java new file mode 100644 index 00000000..4d00a8a0 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeApi.java @@ -0,0 +1,54 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import kr.ac.kookmin.stream.api.admin.AdminApiUser; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticeCreateRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticePinRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticeUpdateRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.response.NoticeCreateResponse; +import kr.ac.kookmin.stream.api.admin.welfare.notice.response.NoticeUpdateResponse; +import kr.ac.kookmin.stream.api.common.dto.ApiResponse; +import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; +import kr.ac.kookmin.stream.common.CommonErrorCode; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeErrorCode; + +/** + * 운영진 공지 API의 문서 명세. 구현은 {@link AdminNoticeController}가 맡는다. + *

+ * 접근은 {@code ADMIN} role이면 된다. 부서 제한은 두지 않았다. + */ +@Tag(name = "공지 관리", description = "운영진 공지 작성·수정·삭제·고정") +public interface AdminNoticeApi { + + /** 공지 작성. 작성자는 토큰의 관리자로 정해진다. */ + @Operation(summary = "공지 작성", + description = "공지를 등록한다. 이미지·첨부 파일은 업로드를 마친 파일 ID로 넘기며, 존재하지 않는 파일이 하나라도 있으면 " + + "등록하지 않는다. 새 공지는 고정되지 않은 상태로 만들어진다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = FileErrorCode.class, codes = {"FILE_NOT_FOUND"}) + ApiResponse createNotice(AdminApiUser apiUser, NoticeCreateRequest request); + + /** 공지 수정. 요청의 수정 시각이 저장된 값과 다르면 거부한다. */ + @Operation(summary = "공지 수정", + description = "공지의 제목·본문·종류·이미지·첨부 파일을 통째로 바꾼다. 수정 화면이 공지를 읽은 뒤 다른 관리자가 먼저 " + + "고쳤다면(수정 시각 불일치) 409로 거부한다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = FileErrorCode.class, codes = {"FILE_NOT_FOUND"}) + @ApiErrorCode(type = NoticeErrorCode.class, codes = {"NOTICE_NOT_FOUND", "NOTICE_UPDATE_CONFLICT"}) + ApiResponse updateNotice(AdminApiUser apiUser, Long noticeId, NoticeUpdateRequest request); + + /** 공지 삭제. 소프트 삭제라 학생 앱 목록·상세에서 보이지 않게 된다. */ + @Operation(summary = "공지 삭제", + description = "공지를 삭제한다. 데이터는 남기고 학생 앱에서만 보이지 않게 한다. 연결된 파일은 지우지 않는다.") + @ApiErrorCode(type = NoticeErrorCode.class, codes = {"NOTICE_NOT_FOUND"}) + ApiResponse deleteNotice(AdminApiUser apiUser, Long noticeId); + + /** 공지 상단 고정 설정/해제. */ + @Operation(summary = "공지 고정 설정/해제", + description = "공지를 목록 맨 위에 고정하거나 고정을 해제한다. 이미 그 상태여도 성공으로 응답한다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = NoticeErrorCode.class, codes = {"NOTICE_NOT_FOUND"}) + ApiResponse changePinned(AdminApiUser apiUser, Long noticeId, NoticePinRequest request); +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java new file mode 100644 index 00000000..2e7c5ce1 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java @@ -0,0 +1,70 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice; + +import jakarta.validation.Valid; +import kr.ac.kookmin.stream.api.admin.AdminApiUser; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticeCreateRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticePinRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.request.NoticeUpdateRequest; +import kr.ac.kookmin.stream.api.admin.welfare.notice.response.NoticeCreateResponse; +import kr.ac.kookmin.stream.api.admin.welfare.notice.response.NoticeUpdateResponse; +import kr.ac.kookmin.stream.api.admin.welfare.notice.usecase.AdminNoticeWriteUseCase; +import kr.ac.kookmin.stream.api.common.dto.ApiResponse; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; +import kr.ac.kookmin.stream.welfare.domain.notice.service.NoticeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/admin/notices") +@RequiredArgsConstructor +public class AdminNoticeController implements AdminNoticeApi { + + private final NoticeService noticeService; + private final AdminNoticeWriteUseCase adminNoticeWriteUseCase; + + @Override + @PostMapping + public ApiResponse createNotice( + AdminApiUser apiUser, + @Valid @RequestBody NoticeCreateRequest request + ) { + Notice notice = adminNoticeWriteUseCase.create(request.toCommand(apiUser.userId())); + return ApiResponse.success(NoticeCreateResponse.from(notice)); + } + + @Override + @PutMapping("/{noticeId}") + public ApiResponse updateNotice( + AdminApiUser apiUser, + @PathVariable Long noticeId, + @Valid @RequestBody NoticeUpdateRequest request + ) { + Notice notice = adminNoticeWriteUseCase.update(noticeId, request.toCommand()); + return ApiResponse.success(NoticeUpdateResponse.from(notice)); + } + + @Override + @DeleteMapping("/{noticeId}") + public ApiResponse deleteNotice(AdminApiUser apiUser, @PathVariable Long noticeId) { + noticeService.delete(noticeId); + return ApiResponse.success(); + } + + @Override + @PatchMapping("/{noticeId}/pin") + public ApiResponse changePinned( + AdminApiUser apiUser, + @PathVariable Long noticeId, + @Valid @RequestBody NoticePinRequest request + ) { + noticeService.changePinned(noticeId, request.pinned()); + return ApiResponse.success(); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java new file mode 100644 index 00000000..751449fc --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java @@ -0,0 +1,49 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCreateCommand; + +/** + * 작성자는 요청 본문이 아니라 토큰의 관리자 ID로 정한다. 본문에 {@code createdBy}가 와도 읽지 않는다. + */ +public record NoticeCreateRequest( + @Schema(description = "제목", example = "제목 예시") + @NotBlank(message = "제목을 입력해 주세요.") + @Size(max = 200, message = "제목은 200자 이하로 입력해 주세요.") + String title, + + // content는 TEXT 컬럼(최대 65,535바이트)이다. 글자당 최대 4바이트로 잡아도 넘치지 않는 길이로 막는다 + @Schema(description = "본문(HTML)", example = "

공지 내용입니다.

") + @NotBlank(message = "본문을 입력해 주세요.") + @Size(max = 16000, message = "본문은 16000자 이하로 입력해 주세요.") + String content, + + @Schema(description = "공지 종류") + @NotNull(message = "공지 종류를 입력해 주세요.") + NoticeCategory category, + + @Schema(description = "이미지 파일 ID 목록. 첫 번째가 목록의 썸네일이 된다", example = "[101, 102]") + @NotNull(message = "이미지 파일 목록을 입력해 주세요.") + List<@NotNull(message = "이미지 파일 ID는 비워 둘 수 없습니다.") Long> imageFileIds, + + @Schema(description = "첨부 파일 ID 목록", example = "[201]") + @NotNull(message = "첨부 파일 목록을 입력해 주세요.") + List<@NotNull(message = "첨부 파일 ID는 비워 둘 수 없습니다.") Long> attachmentFileIds +) { + + public NoticeCreateCommand toCommand(Long createdBy) { + return new NoticeCreateCommand( + title, + content, + category, + imageFileIds.stream().distinct().toList(), + attachmentFileIds.stream().distinct().toList(), + createdBy + ); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticePinRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticePinRequest.java new file mode 100644 index 00000000..16b0a747 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticePinRequest.java @@ -0,0 +1,11 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record NoticePinRequest( + @Schema(description = "고정 여부. true면 목록 맨 위에 고정하고 false면 해제한다", example = "true") + @NotNull(message = "고정 여부를 입력해 주세요.") + Boolean pinned +) { +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java new file mode 100644 index 00000000..12943ef0 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java @@ -0,0 +1,56 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeUpdateCommand; + +public record NoticeUpdateRequest( + @Schema(description = "제목", example = "제목 예시") + @NotBlank(message = "제목을 입력해 주세요.") + @Size(max = 200, message = "제목은 200자 이하로 입력해 주세요.") + String title, + + // content는 TEXT 컬럼(최대 65,535바이트)이다. 글자당 최대 4바이트로 잡아도 넘치지 않는 길이로 막는다 + @Schema(description = "본문(HTML)", example = "

공지 내용입니다.

") + @NotBlank(message = "본문을 입력해 주세요.") + @Size(max = 16000, message = "본문은 16000자 이하로 입력해 주세요.") + String content, + + @Schema(description = "공지 종류") + @NotNull(message = "공지 종류를 입력해 주세요.") + NoticeCategory category, + + @Schema(description = "이미지 파일 ID 목록. 첫 번째가 목록의 썸네일이 된다", example = "[101, 103]") + @NotNull(message = "이미지 파일 목록을 입력해 주세요.") + List<@NotNull(message = "이미지 파일 ID는 비워 둘 수 없습니다.") Long> imageFileIds, + + @Schema(description = "첨부 파일 ID 목록", example = "[201]") + @NotNull(message = "첨부 파일 목록을 입력해 주세요.") + List<@NotNull(message = "첨부 파일 ID는 비워 둘 수 없습니다.") Long> attachmentFileIds, + + @Schema(description = "수정 화면이 공지를 읽었을 때의 수정 시각(KST 기준 epoch 밀리초). 그 사이 다른 관리자가 고쳤다면 409", + example = "1756339200000") + @NotNull(message = "수정 시각을 입력해 주세요.") + Long updatedAt +) { + + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + + public NoticeUpdateCommand toCommand() { + return new NoticeUpdateCommand( + title, + content, + category, + imageFileIds.stream().distinct().toList(), + attachmentFileIds.stream().distinct().toList(), + LocalDateTime.ofInstant(Instant.ofEpochMilli(updatedAt), KST) + ); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeCreateResponse.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeCreateResponse.java new file mode 100644 index 00000000..913dde94 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeCreateResponse.java @@ -0,0 +1,10 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.response; + +import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; + +public record NoticeCreateResponse(Long noticeId) { + + public static NoticeCreateResponse from(Notice notice) { + return new NoticeCreateResponse(notice.getId()); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeUpdateResponse.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeUpdateResponse.java new file mode 100644 index 00000000..87c76103 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/response/NoticeUpdateResponse.java @@ -0,0 +1,10 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.response; + +import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; + +public record NoticeUpdateResponse(Long noticeId) { + + public static NoticeUpdateResponse from(Notice notice) { + return new NoticeUpdateResponse(notice.getId()); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java new file mode 100644 index 00000000..4974fbb2 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java @@ -0,0 +1,48 @@ +package kr.ac.kookmin.stream.api.admin.welfare.notice.usecase; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; +import kr.ac.kookmin.stream.file.service.FileService; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.Notice; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCreateCommand; +import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeUpdateCommand; +import kr.ac.kookmin.stream.welfare.domain.notice.service.NoticeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 공지를 쓰기 전에 연결할 이미지·첨부 파일이 실제로 있는지 확인한다. 공지(welfare)와 파일(file)은 서로 다른 도메인이라 + * 도메인끼리 직접 의존하지 않고 이 UseCase가 두 Service를 조합한다. + *

+ * 파일 확인과 공지 저장은 한 트랜잭션으로 묶지 않는다. 확인 직후 파일이 지워져도 공지 조회가 삭제된 파일을 + * 목록에서 조용히 제외하므로 원자성이 필요하지 않다. + */ +@Component +@RequiredArgsConstructor +public class AdminNoticeWriteUseCase { + + private final NoticeService noticeService; + private final FileService fileService; + + public Notice create(NoticeCreateCommand command) { + requireFilesExist(command.imageIds(), command.attachmentIds()); + return noticeService.create(command); + } + + public Notice update(Long noticeId, NoticeUpdateCommand command) { + requireFilesExist(command.imageIds(), command.attachmentIds()); + return noticeService.update(noticeId, command); + } + + private void requireFilesExist(List imageIds, List attachmentIds) { + Set fileIds = new LinkedHashSet<>(imageIds); + fileIds.addAll(attachmentIds); + + if (fileService.findAllByIdIn(List.copyOf(fileIds)).size() != fileIds.size()) { + throw new BusinessException(FileErrorCode.FILE_NOT_FOUND); + } + } +} From f870ae2f285181687f4c6ac53c64bc4c416db23e Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Mon, 12 Oct 2026 10:44:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=A7=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20ID=EB=A5=BC=20Command=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=B4=20=EB=B3=84=EB=8F=84=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/admin/welfare/notice/AdminNoticeController.java | 2 +- .../admin/welfare/notice/request/NoticeCreateRequest.java | 5 ++--- .../welfare/notice/usecase/AdminNoticeWriteUseCase.java | 4 ++-- .../kookmin/stream/welfare/domain/notice/domain/Notice.java | 4 ++-- .../welfare/domain/notice/domain/NoticeCreateCommand.java | 3 +-- .../stream/welfare/domain/notice/service/NoticeService.java | 2 +- .../domain/notice/service/impl/NoticeServiceImpl.java | 4 ++-- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java index 2e7c5ce1..bd84a8ec 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/AdminNoticeController.java @@ -35,7 +35,7 @@ public ApiResponse createNotice( AdminApiUser apiUser, @Valid @RequestBody NoticeCreateRequest request ) { - Notice notice = adminNoticeWriteUseCase.create(request.toCommand(apiUser.userId())); + Notice notice = adminNoticeWriteUseCase.create(apiUser.userId(), request.toCommand()); return ApiResponse.success(NoticeCreateResponse.from(notice)); } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java index 751449fc..11c1a912 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeCreateRequest.java @@ -36,14 +36,13 @@ public record NoticeCreateRequest( List<@NotNull(message = "첨부 파일 ID는 비워 둘 수 없습니다.") Long> attachmentFileIds ) { - public NoticeCreateCommand toCommand(Long createdBy) { + public NoticeCreateCommand toCommand() { return new NoticeCreateCommand( title, content, category, imageFileIds.stream().distinct().toList(), - attachmentFileIds.stream().distinct().toList(), - createdBy + attachmentFileIds.stream().distinct().toList() ); } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java index 4974fbb2..ef045128 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java @@ -27,9 +27,9 @@ public class AdminNoticeWriteUseCase { private final NoticeService noticeService; private final FileService fileService; - public Notice create(NoticeCreateCommand command) { + public Notice create(Long createdBy, NoticeCreateCommand command) { requireFilesExist(command.imageIds(), command.attachmentIds()); - return noticeService.create(command); + return noticeService.create(createdBy, command); } public Notice update(Long noticeId, NoticeUpdateCommand command) { diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java index f8923553..bd7f2ccf 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/Notice.java @@ -39,14 +39,14 @@ public static Notice of( } // 새 공지는 고정되지 않은 채로 등록한다. 생성·수정 시각은 DB가 채운다 - public static Notice create(NoticeCreateCommand command) { + public static Notice create(Long createdBy, NoticeCreateCommand command) { return new Notice( null, command.title(), command.content(), command.category(), false, - command.createdBy(), + createdBy, command.attachmentIds(), command.imageIds(), null, diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java index 0985a56c..4979a048 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/domain/NoticeCreateCommand.java @@ -7,7 +7,6 @@ public record NoticeCreateCommand( String content, NoticeCategory category, List imageIds, - List attachmentIds, - Long createdBy + List attachmentIds ) { } diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java index 8b678c64..6e1e1af4 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/NoticeService.java @@ -10,7 +10,7 @@ public interface NoticeService { CursorSliceResult getNotices(NoticeCategory category, NoticeCursor cursor, int size); Notice getNotice(Long id); - Notice create(NoticeCreateCommand command); + Notice create(Long createdBy, NoticeCreateCommand command); /** * 공지를 수정한다. 명령의 수정 시각이 저장된 값과 다르면(그 사이 다른 관리자가 고친 경우) diff --git a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java index c8094464..f5d5d39f 100644 --- a/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java +++ b/core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/notice/service/impl/NoticeServiceImpl.java @@ -40,8 +40,8 @@ public Notice getNotice(Long id) { } @Override - public Notice create(NoticeCreateCommand command) { - return noticeRepository.save(Notice.create(command)); + public Notice create(Long createdBy, NoticeCreateCommand command) { + return noticeRepository.save(Notice.create(createdBy, command)); } @Override From 3e7ee66816eaea1b8fc9670d65008c30d59378a8 Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Mon, 12 Oct 2026 10:44:49 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=ED=99=95=EC=9D=B8=EC=97=90=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20HashSet=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java index ef045128..7240df26 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java @@ -1,6 +1,6 @@ package kr.ac.kookmin.stream.api.admin.welfare.notice.usecase; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import java.util.Set; import kr.ac.kookmin.stream.common.BusinessException; @@ -38,7 +38,7 @@ public Notice update(Long noticeId, NoticeUpdateCommand command) { } private void requireFilesExist(List imageIds, List attachmentIds) { - Set fileIds = new LinkedHashSet<>(imageIds); + Set fileIds = new HashSet<>(imageIds); fileIds.addAll(attachmentIds); if (fileService.findAllByIdIn(List.copyOf(fileIds)).size() != fileIds.size()) { From 8b591f159e08d0c9c0bef0e20242e970aa0dd2d3 Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Mon, 12 Oct 2026 10:52:42 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=A7=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=8B=9C=EA=B0=81=20=EB=B3=80=ED=99=98=EC=97=90=20?= =?UTF-8?q?DateUtil.KST=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/welfare/notice/request/NoticeUpdateRequest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java index 12943ef0..64ed9905 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/request/NoticeUpdateRequest.java @@ -6,8 +6,8 @@ import jakarta.validation.constraints.Size; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneId; import java.util.List; +import kr.ac.kookmin.stream.common.DateUtil; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeCategory; import kr.ac.kookmin.stream.welfare.domain.notice.domain.NoticeUpdateCommand; @@ -41,8 +41,6 @@ public record NoticeUpdateRequest( Long updatedAt ) { - private static final ZoneId KST = ZoneId.of("Asia/Seoul"); - public NoticeUpdateCommand toCommand() { return new NoticeUpdateCommand( title, @@ -50,7 +48,7 @@ public NoticeUpdateCommand toCommand() { category, imageFileIds.stream().distinct().toList(), attachmentFileIds.stream().distinct().toList(), - LocalDateTime.ofInstant(Instant.ofEpochMilli(updatedAt), KST) + LocalDateTime.ofInstant(Instant.ofEpochMilli(updatedAt), DateUtil.KST) ); } } From e1d03e9bc400b4dabeb9709edf8ed3eaecdcd9fe Mon Sep 17 00:00:00 2001 From: jjunh33 Date: Mon, 12 Oct 2026 14:21:40 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20ID=20?= =?UTF-8?q?=ED=95=A9=EC=B9=98=EA=B8=B0=EB=A5=BC=20Stream.concat=EA=B3=BC?= =?UTF-8?q?=20=EB=B6=88=EB=B3=80=20Set=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../welfare/notice/usecase/AdminNoticeWriteUseCase.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java index 7240df26..a1a5f2b3 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/welfare/notice/usecase/AdminNoticeWriteUseCase.java @@ -1,8 +1,9 @@ package kr.ac.kookmin.stream.api.admin.welfare.notice.usecase; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import kr.ac.kookmin.stream.common.BusinessException; import kr.ac.kookmin.stream.file.domain.FileErrorCode; import kr.ac.kookmin.stream.file.service.FileService; @@ -38,8 +39,8 @@ public Notice update(Long noticeId, NoticeUpdateCommand command) { } private void requireFilesExist(List imageIds, List attachmentIds) { - Set fileIds = new HashSet<>(imageIds); - fileIds.addAll(attachmentIds); + Set fileIds = Stream.concat(imageIds.stream(), attachmentIds.stream()) + .collect(Collectors.toUnmodifiableSet()); if (fileService.findAllByIdIn(List.copyOf(fileIds)).size() != fileIds.size()) { throw new BusinessException(FileErrorCode.FILE_NOT_FOUND);