Skip to content

[Feat/#185] 아카이빙 목록·상세 조회 관리자 API 추가 - #188

Open
sangrae2325 wants to merge 5 commits into
mainfrom
feat/#185-admin-archive-queries
Open

sangrae2325 wants to merge 5 commits into
mainfrom
feat/#185-admin-archive-queries

Conversation

@sangrae2325

@sangrae2325 sangrae2325 commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

운영진 콘솔에서 아카이빙을 찾고 열어볼 수 있게 조회 API 2개를 추가하고, 그에 필요한 슬랑제 노출 여부·소프트 삭제 컬럼을 넣습니다.

GET /v1/admin/archives?keyword=&isExposed=&page=&size=
GET /v1/admin/archives/{archiveId}

슬랑제는 소프트웨어융합대학 연말 행사입니다. 전용 페이지를 따로 만들어 아카이빙에 등록된 것 중 고른 것만 노출하는데, 이번 작업은 노출 여부를 저장하고 운영진이 목록에서 보고 걸러내는 것까지입니다.

❓ 왜 해결해야 하나요?

archives 테이블과 archive 도메인은 #40에서 학생 앱 조회용으로만 만들어졌습니다. ArchiveService에 쓰기가 하나도 없고 AdminArchiveService도 없으며, 관리자 화면이 쓰는 값이 테이블에 빠져 있습니다.

필요한 것 현재
슬랑제 페이지 노출 여부 컬럼이 없다
삭제 is_deleted가 없다

⭐ 어떻게 해결했나요?

커밋 5개로 나눴고 각각 단독으로 컴파일됩니다.

1. 스키마(V27) — is_exposed·is_deleted를 넣습니다. 운영 중인 행이 있어 둘 다 DEFAULT를 줍니다.

인덱스는 두지 않습니다. 아카이빙이 연간 수십 건 규모라 목록이 테이블을 통째로 읽어도 되고, 제목 검색이 LIKE '%...%'라 어차피 인덱스를 타지 못합니다.

컬럼 이름에 "슬랑제"를 넣지 않았습니다. 아카이빙이 노출될 곳은 슬랑제 페이지뿐이라 is_exposed로 충분하고, 어디에 노출되는지는 마이그레이션 주석과 도메인 javadoc이 설명합니다.

소프트 삭제는 BaseSoftDeleteEntity가 들고 도메인에는 넣지 않습니다 — Notice·Event 도메인도 삭제 플래그를 들고 있지 않습니다. 반면 노출 여부는 운영진이 목록에서 보고 켜고 끄는 값이라 도메인에 둡니다.

2. 기존 앱 조회에 삭제 필터 — 목록·연도 목록·상세 세 군데입니다. 연도 목록(findAllYearsDesc)은 목록과 별개 쿼리라 놓치면 지운 아카이빙의 연도가 필터 버튼에 남습니다.

3~4. 저장소·서비스 — 제목 부분 일치 + 노출 여부 + 등록일 최신순 페이징. AdminArchiveService를 새로 만들어 학생용 ArchiveService와 나눕니다.

나누는 기준은 "조회가 실제로 다른가"입니다. 목록은 정렬 기준이 달라(등록일 vs 활동 시작일) 쿼리가 다르지만, 상세는 한 글자도 안 달라서 운영진도 ArchiveService를 그대로 씁니다. coding-style.md 2-7절의 공지와 같은 모양입니다.

5. API 계층 — 컨트롤러가 목록은 AdminArchiveService, 상세는 ArchiveService를 부릅니다.

쿼리 파라미터를 record로 감쌌습니다

Boolean을 @RequestParam으로 직접 받으면 ?isExposed=abc에 500이 납니다. MethodArgumentTypeMismatchException은 TypeMismatchException 자식이라 GlobalExceptionHandler의 IllegalArgumentException 핸들러에 걸리지 않고 catch-all로 빠집니다. @Valid @ModelAttribute로 받으면 바인딩 실패가 MethodArgumentNotValidException으로 나와 400 INVALID_INPUT이 됩니다(ModelAttributeMethodProcessor가 그 예외를 던지는 것을 확인했습니다).

LockerPeriodParams·ArchiveListParams가 같은 이유로 record를 쓰고 있습니다.

🧩 이 PR의 한계 & 트레이드오프

  • 삭제 API가 #186에 있습니다. 이 PR만 머지되면 is_deleted를 1로 만들 방법이 없어, 필터가 동작하는지 보려면 DB를 직접 고쳐야 합니다
  • 제목 검색이 테이블 전체를 훑습니다. LIKE '%...%'는 인덱스를 타지 못합니다. 아카이빙이 연간 수십 건 규모라 그대로 뒀고, 커지면 전문 검색을 따로 봅니다
  • 인덱스를 하나도 두지 않았습니다. 목록이 created_at 최신순이라 (is_deleted, created_at)을 넣을까 했는데, 아카이빙이 연간 수십 건 규모라 테이블을 통째로 읽어도 되고 제목 검색이 LIKE '%...%'라 어차피 인덱스를 못 탑니다. 행이 늘면 다시 봅니다
  • 한 메서드에 @ModelAttribute가 둘입니다(ArchiveSearchParams·PageParams). 저장소에 선례가 없는 모양인데, 필터를 한 덩어리로 묶는 쪽이 읽기 좋다고 봤습니다
  • GET /{archiveId}에 숫자가 아닌 값을 넣으면 500입니다. @PathVariable Long도 같은 MethodArgumentTypeMismatchException을 내는데 핸들러가 없습니다. 저장소 전체 공통 문제라(/v1/admin/notices/{noticeId} 등) common-api의 GlobalExceptionHandler를 건드려야 해서 이 PR 범위에서 뺐습니다. 후속 이슈로 보면 좋겠습니다
  • 테스트 코드는 없습니다. AGENTS.md 원칙에 따랐습니다

⛓️ 기존 기능에 미치는 영향

  • 학생 앱 응답 모양은 바뀌지 않습니다. 조회 세 곳에 is_deleted = 0만 붙고, 지금은 삭제된 행이 없어 결과가 같습니다
  • Archive에 createdAt이 올라옵니다. created_at은 V2부터 있던 컬럼인데 도메인에 없었습니다. 운영진 목록이 이 값으로 정렬하고 화면에도 보여줍니다(Notice 도메인도 createdAt을 들고 있습니다)
  • ArchiveDetail에 exposed·createdAt이 늘어납니다. 운영진만 쓰고 학생 앱 응답은 담지 않습니다
  • admin-api에 core:domain:event 의존이 생깁니다. 운영진 콘솔의 첫 event 기능입니다. 사물함 스택([Feat/#119] 사물함 운영 회차 관리자 API 추가 #129)도 같은 줄을 추가하므로 나중에 머지하는 쪽에서 한 줄 충돌이 납니다
  • ArchiveErrorCode·기존 에러 코드는 그대로입니다

🔀 Edge Case & 실패 시나리오

상황 동작
keyword에 %나 _ 글자 그대로 검색 — LikePatterns.escape + 쿼리의 ESCAPE '!'
keyword가 공백뿐 검색하지 않는 것과 같다 (escape가 null로 바꾼다)
isExposed=abc INVALID_INPUT (400)
isExposed 생략 노출 여부로 좁히지 않는다
page가 음수 / size가 101 INVALID_INPUT (400) — PageParams의 @Min/@Max
결과가 0건 빈 페이지
없는 아카이빙 상세 ARCHIVE_NOT_FOUND (404)
삭제된 아카이빙 상세 ARCHIVE_NOT_FOUND (404) — 없는 것과 같게 본다
사진 파일이 삭제됨 사진 목록에서 조용히 빠진다
사진이 한 장도 없음 images가 빈 배열

📋 검토한 대안과 선택 이유

  • 상세 조회도 AdminArchiveService에 두기: 처음엔 그렇게 썼다가 접었습니다. 내용이 학생 앱과 한 글자도 다르지 않아 복사본이 생깁니다. coding-style.md 2-7절이 공지를 예로 "상세 조회는 운영진도 공용"이라고 적고 있어 그대로 맞췄습니다
  • 운영진 목록용 읽기 모델(AdminArchiveSummary)을 두기: 만들었다가 지웠습니다. Archive의 필드를 그대로 베끼기만 하고 계산이 없어서, AdminNoticeController가 PageResult<Notice>를 받아 매핑하는 것과 같이 도메인 객체를 그대로 넘깁니다
  • 대표 이미지를 응답에 따로 담기: 사진 배열의 첫 장이 대표라 화면이 떼어 쓰면 됩니다. 따로 담으면 images[0]과 중복되고, 학생 앱 상세(ArchiveDetailResponse)와 모양도 달라집니다
  • thumbnail_file_id 컬럼을 만들기: 대표를 사진과 별개 파일로 두는 방식인데, 지금처럼 "첫 장이 대표"면 컬럼이 필요 없고 관리자 화면의 대표 선택은 사진 순서로 처리됩니다
  • departmentName을 CouncilDepartment enum으로 묶기: 화면 예시("대외협력국·학생복지국·문화기획국")가 enum의 8개 부서명(회장단·집행부·총무부·기획부·홍보부·미디어부·복지부·소통부)과 맞지 않아 자유 문자열로 뒀습니다

💬 리뷰 포인트

  • [c] V27 번호와 머지 순서를 봐주세요. main이 V19까지인데 사물함 스택이 V20·V21·V26, 행사 스택이 V20V22, #140이 V22, #167이 V23V25를 들고 있습니다. outOfOrder가 꺼져 있어서 이 PR이 그 스택들보다 먼저 머지되면 뒤에 올라오는 낮은 번호를 Flyway가 거부합니다. 뒤로 미루거나 번호를 다시 잡아야 합니다
  • [c] 학생 앱 조회 세 곳에 붙인 is_deleted = 0이 빠진 데가 없는지 봐주세요. 연도 목록이 별개 쿼리라 놓치기 쉬운 자리였습니다
  • [a] 쿼리 파라미터를 ArchiveSearchParams로 감싼 것 — @RequestParam 두 개로 두면 isExposed=abc에 500이 납니다. keyword까지 같이 묶었는데, 공지처럼 keyword만 @RequestParam으로 두는 쪽이 나으면 바꾸겠습니다

@sangrae2325 sangrae2325 self-assigned this Oct 29, 2026
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

아카이빙 목록·상세 조회 관리자 API 추가

1 participant