Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ ApiResponse<PageResponse<AdminArchiveListItemResponse>> getArchives(
@Operation(summary = "아카이빙 상세 조회",
description = "아카이빙 한 건을 조회한다. 당일 활동은 endDate가 null이다. "
+ "활동 사진은 노출 순서대로 담고 첫 장이 대표 이미지다 — 대표를 따로 내려주지 않는다. "
+ "삭제된 파일은 사진 목록에서 조용히 빠진다.")
+ "삭제된 파일은 사진 목록에서 조용히 빠진다. "
+ "relatedEvent는 연결한 행사이고, 제목은 저장하지 않고 조회할 때마다 원본에서 가져온다. "
+ "연결하지 않았거나 행사가 삭제됐으면 null이다 — 운영진 화면이므로 아직 게시하지 않은 행사도 보여준다.")
@ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"})
ApiResponse<AdminArchiveDetailResponse> getArchive(AdminApiUser apiUser, Long archiveId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveSearchParams;
import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveDetailResponse;
import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveListItemResponse;
import kr.ac.kookmin.stream.api.admin.event.archive.usecase.AdminArchiveDetailUseCase;
import kr.ac.kookmin.stream.api.admin.event.archive.usecase.ArchiveWithRelatedEvent;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.api.common.dto.PageParams;
import kr.ac.kookmin.stream.api.common.dto.PageResponse;
import kr.ac.kookmin.stream.common.PageResult;
import kr.ac.kookmin.stream.event.domain.archive.domain.Archive;
import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;
import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService;
import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService;
import kr.ac.kookmin.stream.file.domain.File;
import kr.ac.kookmin.stream.file.service.FileService;
import lombok.RequiredArgsConstructor;
Expand All @@ -33,7 +33,7 @@
public class AdminArchiveController implements AdminArchiveApi {

private final AdminArchiveService adminArchiveService;
private final ArchiveService archiveService;
private final AdminArchiveDetailUseCase adminArchiveDetailUseCase;
private final FileService fileService;

@Override
Expand All @@ -55,9 +55,10 @@ public ApiResponse<AdminArchiveDetailResponse> getArchive(
AdminApiUser apiUser,
@PathVariable Long archiveId
) {
ArchiveDetail detail = archiveService.getArchive(archiveId);
Map<Long, File> imagesById = fileService.findAllByIdIn(detail.imageIds());
ArchiveWithRelatedEvent archive = adminArchiveDetailUseCase.getArchive(archiveId);
Map<Long, File> imagesById = fileService.findAllByIdIn(archive.detail().imageIds());

return ApiResponse.success(AdminArchiveDetailResponse.of(detail, imagesById));
return ApiResponse.success(
AdminArchiveDetailResponse.of(archive.detail(), archive.relatedEventTitle(), imagesById));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ public record AdminArchiveDetailResponse(
String content,
boolean isExposed,
List<Image> images,
RelatedEvent relatedEvent,
LocalDateTime createdAt
) {

/** @param imagesById 활동 사진 파일. 삭제된 파일은 키가 없어 목록에서 조용히 빠진다 */
public static AdminArchiveDetailResponse of(ArchiveDetail detail, Map<Long, File> imagesById) {
/**
* @param relatedEventTitle 연결한 행사의 제목. 연결이 없거나 행사가 사라졌으면 {@code null}
* @param imagesById 활동 사진 파일. 삭제된 파일은 키가 없어 목록에서 조용히 빠진다
*/
public static AdminArchiveDetailResponse of(
ArchiveDetail detail, String relatedEventTitle, Map<Long, File> imagesById
) {
return new AdminArchiveDetailResponse(
detail.archiveId(),
detail.title(),
Expand All @@ -43,10 +49,20 @@ public static AdminArchiveDetailResponse of(ArchiveDetail detail, Map<Long, File
.filter(imagesById::containsKey)
.map(fileId -> Image.of(fileId, imagesById.get(fileId)))
.toList(),
RelatedEvent.of(detail.relatedEventId(), relatedEventTitle),
detail.createdAt()
);
}

/** 연결한 행사. 수정 화면이 드롭다운의 기존 값을 채울 때도 쓴다. */
public record RelatedEvent(Long eventId, String title) {

/** 연결이 없거나 행사가 사라져 제목을 못 가져왔으면 {@code null}이다. */
public static RelatedEvent of(Long eventId, String title) {
return eventId == null || title == null ? null : new RelatedEvent(eventId, title);
}
}

public record Image(Long fileId, String fileUrl) {

public static Image of(Long fileId, File file) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kr.ac.kookmin.stream.api.admin.event.archive.usecase;

import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;
import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService;
import kr.ac.kookmin.stream.event.domain.event.domain.Event;
import kr.ac.kookmin.stream.event.domain.event.service.EventService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* 아카이빙 상세에 연결한 행사의 제목을 붙인다. 아카이빙과 행사는 같은 모듈의 서로 다른 도메인이라 서비스끼리
* 부르지 않고 이 UseCase가 조합한다({@code architecture.md} 6-1절, 레이어 규칙 4).
* <p>
* 상세 조회 자체는 학생 앱과 내용이 같아 {@link ArchiveService}를 그대로 쓴다({@code coding-style.md} 2-7절).
* 학생 앱 쪽({@code ArchiveDetailUseCase})과 달리 <b>미게시 행사도 보여준다</b> — 운영진은 아직 공개하지 않은
* 행사도 연결 대상으로 확인할 수 있어야 한다.
*/
@Component
@RequiredArgsConstructor
public class AdminArchiveDetailUseCase {

private final ArchiveService archiveService;
private final EventService eventService;

public ArchiveWithRelatedEvent getArchive(Long archiveId) {
ArchiveDetail detail = archiveService.getArchive(archiveId);
return new ArchiveWithRelatedEvent(detail, resolveTitle(detail.relatedEventId()));
}

/**
* 연결한 행사가 지워졌으면 {@code null}을 돌려준다. 아카이빙까지 404로 만들지 않고 연결만 빼서 보여준다.
*/
private String resolveTitle(Long relatedEventId) {
if (relatedEventId == null) {
return null;
}

Event event = eventService.getByIdOrNull(relatedEventId);
return event == null ? null : event.getTitle();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.ac.kookmin.stream.api.admin.event.archive.usecase;

import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;

/**
* {@link AdminArchiveDetailUseCase}가 조합한 결과. DTO 변환은 Controller가 한다.
* <p>
* {@code api:app-api}에도 같은 이름의 record가 있다. 두 모듈은 서로를 모르고, 몇 줄을 공유하자고
* {@code api:common-api}가 event 도메인에 의존하게 만들 이유가 없어 복제해 둔다 — 운영진과 학생 앱의
* 연결 행사 판정이 이미 갈려 있어(미게시 행사) 앞으로 더 달라질 자리이기도 하다.
*
* @param relatedEventTitle 연결한 행사의 제목. 연결이 없거나 행사가 사라졌으면 {@code null}
*/
public record ArchiveWithRelatedEvent(ArchiveDetail detail, String relatedEventTitle) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ public interface AppArchiveApi {
@ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"})
ApiResponse<ArchiveListResponse> getArchives(@ParameterObject ArchiveListParams params);

/** 아카이브 상세. */
@Operation(summary = "행사 아카이브 상세 조회")
/** 아카이브 상세. 연결된 행사가 있으면 눌러서 이동할 수 있게 함께 내려준다. */
@Operation(summary = "행사 아카이브 상세 조회",
description = "아카이브 한 건을 조회한다. 현장 사진은 저장된 순서가 곧 표시 순서이고, 삭제된 파일은 "
+ "조용히 빠진다. relatedEvent는 연결한 행사이고 제목은 조회할 때마다 원본에서 가져와 항상 최신이다. "
+ "연결하지 않았거나 행사가 삭제·미게시면 null이다.")
@ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"})
ApiResponse<ArchiveDetailResponse> getArchive(Long archiveId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import kr.ac.kookmin.stream.api.app.event.archive.request.ArchiveListParams;
import kr.ac.kookmin.stream.api.app.event.archive.response.ArchiveDetailResponse;
import kr.ac.kookmin.stream.api.app.event.archive.response.ArchiveListResponse;
import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;
import kr.ac.kookmin.stream.api.app.event.archive.usecase.ArchiveDetailUseCase;
import kr.ac.kookmin.stream.api.app.event.archive.usecase.ArchiveWithRelatedEvent;
import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveSummary;
import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService;
import kr.ac.kookmin.stream.file.domain.File;
Expand All @@ -25,6 +26,7 @@
public class AppArchiveController implements AppArchiveApi {

private final ArchiveService archiveService;
private final ArchiveDetailUseCase archiveDetailUseCase;
private final FileService fileService;

/**
Expand All @@ -46,9 +48,10 @@ public ApiResponse<ArchiveListResponse> getArchives(@Valid @ModelAttribute Archi
@Override
@GetMapping("/{archiveId}")
public ApiResponse<ArchiveDetailResponse> getArchive(@PathVariable Long archiveId) {
ArchiveDetail detail = archiveService.getArchive(archiveId);
Map<Long, File> imagesById = fileService.findAllByIdIn(detail.imageIds());
ArchiveWithRelatedEvent archive = archiveDetailUseCase.getArchive(archiveId);
Map<Long, File> imagesById = fileService.findAllByIdIn(archive.detail().imageIds());

return ApiResponse.success(ArchiveDetailResponse.from(detail, imagesById));
return ApiResponse.success(
ArchiveDetailResponse.of(archive.detail(), archive.relatedEventTitle(), imagesById));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.Map;
import kr.ac.kookmin.stream.api.common.StorageUrlBuilder;
import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;
import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveRelatedLink;
import kr.ac.kookmin.stream.file.domain.File;

public record ArchiveDetailResponse(
Expand All @@ -17,13 +16,17 @@ public record ArchiveDetailResponse(
String departmentName,
String content,
List<Image> images,
List<RelatedLink> relatedLinks
RelatedEvent relatedEvent
) {

/**
* 존재하지 않는(삭제된) fileId는 imagesById에 키가 없다 — 그런 항목은 목록에서 조용히 제외한다.
*
* @param relatedEventTitle 연결한 행사의 제목. 연결이 없거나 행사가 사라졌으면 {@code null}
*/
public static ArchiveDetailResponse from(ArchiveDetail detail, Map<Long, File> imagesById) {
public static ArchiveDetailResponse of(
ArchiveDetail detail, String relatedEventTitle, Map<Long, File> imagesById
) {
return new ArchiveDetailResponse(
detail.archiveId(),
detail.title(),
Expand All @@ -36,21 +39,23 @@ public static ArchiveDetailResponse from(ArchiveDetail detail, Map<Long, File> i
.filter(imagesById::containsKey)
.map(fileId -> Image.of(fileId, imagesById.get(fileId)))
.toList(),
detail.relatedLinks().stream().map(RelatedLink::from).toList()
RelatedEvent.of(detail.relatedEventId(), relatedEventTitle)
);
}

public record Image(Long fileId, String fileUrl) {
/** 눌러서 이동할 행사. */
public record RelatedEvent(Long eventId, String title) {

public static Image of(Long fileId, File file) {
return new Image(fileId, StorageUrlBuilder.build(file.getFileKey()));
/** 연결이 없거나 행사가 사라져 제목을 못 가져왔으면 {@code null}이다. */
public static RelatedEvent of(Long eventId, String title) {
return eventId == null || title == null ? null : new RelatedEvent(eventId, title);
}
}

public record RelatedLink(String title, String url) {
public record Image(Long fileId, String fileUrl) {

public static RelatedLink from(ArchiveRelatedLink relatedLink) {
return new RelatedLink(relatedLink.getTitle(), relatedLink.getUrl());
public static Image of(Long fileId, File file) {
return new Image(fileId, StorageUrlBuilder.build(file.getFileKey()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kr.ac.kookmin.stream.api.app.event.archive.usecase;

import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;
import kr.ac.kookmin.stream.event.domain.archive.service.ArchiveService;
import kr.ac.kookmin.stream.event.domain.event.domain.Event;
import kr.ac.kookmin.stream.event.domain.event.service.EventService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* 아카이빙 상세에 연결한 행사의 제목을 붙인다. 아카이빙과 행사는 같은 모듈의 서로 다른 도메인이라 서비스끼리
* 부르지 않고 이 UseCase가 조합한다({@code architecture.md} 6-1절, 레이어 규칙 4).
* <p>
* 제목을 저장하지 않고 조회할 때마다 가져오므로 행사명을 고쳐도 아카이빙 화면에 최신 제목이 뜬다.
*/
@Component
@RequiredArgsConstructor
public class ArchiveDetailUseCase {

private final ArchiveService archiveService;
private final EventService eventService;

public ArchiveWithRelatedEvent getArchive(Long archiveId) {
ArchiveDetail detail = archiveService.getArchive(archiveId);
return new ArchiveWithRelatedEvent(detail, resolveTitle(detail.relatedEventId()));
}

/**
* 연결한 행사가 지워졌으면 {@code null}을 돌려준다. 아카이빙까지 404로 만들지 않고 연결만 빼서 보여준다.
* 아직 게시하지 않은 행사도 학생에게는 없는 것으로 본다.
*/
private String resolveTitle(Long relatedEventId) {
if (relatedEventId == null) {
return null;
}

Event event = eventService.getByIdOrNull(relatedEventId);
return event == null || !event.isPublished() ? null : event.getTitle();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package kr.ac.kookmin.stream.api.app.event.archive.usecase;

import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveDetail;

/**
* {@link ArchiveDetailUseCase}가 조합한 결과. DTO 변환은 Controller가 한다.
* <p>
* {@code api:admin-api}에도 같은 record가 있다. 복제해 둔 이유는 그쪽 javadoc에 적었다.
*
* @param relatedEventTitle 연결한 행사의 제목. 연결이 없거나 행사가 사라졌으면 {@code null}
*/
public record ArchiveWithRelatedEvent(ArchiveDetail detail, String relatedEventTitle) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class Archive {
private List<Long> imageIds;
/** 슬랑제(연말 행사) 전용 페이지에 띄울지. 삭제 여부와 달리 운영진이 목록에서 보고 켜고 끄는 값이라 도메인이 들고 있다. */
private boolean exposed;
/** 연결한 행사. 연결하지 않았으면 {@code null}이다. 제목은 담지 않는다 — 조회 시점에 행사에서 가져온다. */
private Long relatedEventId;
/** 등록 일시. 운영진 목록이 이 순서로 정렬하고 화면에도 보여준다. */
private LocalDateTime createdAt;

Expand All @@ -36,10 +38,11 @@ public static Archive of(
String departmentName,
List<Long> imageIds,
boolean exposed,
Long relatedEventId,
LocalDateTime createdAt
) {
return new Archive(
id, title, content, startDate, endDate, location, departmentName, imageIds, exposed, createdAt);
return new Archive(id, title, content, startDate, endDate, location, departmentName, imageIds,
exposed, relatedEventId, createdAt);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* <p>
* 학생 앱과 운영진이 같이 쓴다. 담는 값이 거의 같고 운영진만 보는 것은 두 개(슬랑제 노출 여부·등록 일시)뿐이라
* 하나로 두고, 학생 앱 응답이 그 둘을 담지 않는다.
*
* @param relatedEventId 연결한 행사. 연결하지 않았으면 {@code null}이고, 제목은 담지 않는다
*/
public record ArchiveDetail(
Long archiveId,
Expand All @@ -19,12 +21,12 @@ public record ArchiveDetail(
String departmentName,
String content,
List<Long> imageIds,
List<ArchiveRelatedLink> relatedLinks,
Long relatedEventId,
boolean exposed,
LocalDateTime createdAt
) {

public static ArchiveDetail of(Archive archive, List<ArchiveRelatedLink> relatedLinks) {
public static ArchiveDetail from(Archive archive) {
return new ArchiveDetail(
archive.getId(),
archive.getTitle(),
Expand All @@ -34,7 +36,7 @@ public static ArchiveDetail of(Archive archive, List<ArchiveRelatedLink> related
archive.getDepartmentName(),
archive.getContent(),
archive.getImageIds() == null ? List.of() : archive.getImageIds(),
relatedLinks,
archive.getRelatedEventId(),
archive.isExposed(),
archive.getCreatedAt()
);
Expand Down

This file was deleted.

Loading