Skip to content

feat(admin): 지원자 목록 엑셀(xlsx) 다운로드 #163 - #166

Merged
tlgms merged 5 commits into
developfrom
feature/163-applicant-list-excel
Sep 14, 2026
Merged

tlgms merged 5 commits into
developfrom
feature/163-applicant-list-excel

Conversation

@tlgms

@tlgms tlgms commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • 관리자가 지원자 목록을 엑셀(xlsx)로 내려받게 한다. 새 API 를 만들지 않고, 이미 있던 비동기 내보내기(APPLICANT_LIST)의 산출물을 CSV 에서 xlsx 로 바꿨다.
순서 admin HTTP 결과
1 POST /api/v11/admin/exports {"type": "APPLICANT_LIST", "filter": {...}} 202 + exportJobId
2 GET /api/v11/admin/exports/{exportJobId} COMPLETED 이면 downloadUrl (15분 유효)
3 GET {downloadUrl} applicants_<exportJobId>.xlsx
  • 내보내기 필터가 저장 과정에서 사라져 필터와 상관없이 전체 지원자가 나가던 버그도 고쳤다 (수험표 묶음 포함)
  • API 명세: documents/features/applicant-list-excel/api-spec.md

Related Issue

Scope

  • In scope: admin 도메인 포트(XlsxRenderPort)·POI 어댑터·내보내기 처리기·요청 DTO, 테스트, API 명세
  • Out of scope: 동기 다운로드 API, 다운로드 파일명(Content-Disposition)·열 너비·셀 서식, 필터 DB 저장(재시작 뒤 재처리), configuration 의 지원자 명단 API 와 연결, admin 지원자 테이블 데이터 경로(아래 확인 부탁 1), Notion API 명세 DB 반영

Implementation

흐름은 그대로 두고 산출물만 바꿨다.

  • Notion 명세의 "내보내기 (Export)" 행에 비동기 작업 패턴으로 통일하라고 적혀 있고, 처리기에도 "엑셀 서식이 필요해지면 그때 POI를 붙인다"는 주석이 있었다
  • xlsx 생성은 adapter-out 이 맡는다. 수험표의 PdfRenderPort(HTML → PDF)처럼 도메인에 XlsxRenderPort(머리글 + 행 → xlsx)를 두고 PoiXlsxAdapter 로 구현했다. 어떤 열을 담을지는 처리기가 정한다
  • POI 5.4.0 은 이미 kotlin.MODULE.bazel 에 있고 observability 가 쓰고 있어, admin-adapter-out deps.bzl 에만 추가했다

엑셀 구성

  • 시트 지원자 목록, 1행 머리글(틀 고정), 접수번호 오름차순
  • 15열: 접수번호, 수험번호, 성명, 생년월일, 연락처, 지역, 전형, 학력, 출신학교, 원서 도착, 상태, 교과·출결·봉사 점수, 총점 (CSV 9열에 생년월일·연락처·출신학교·항목별 점수를 더함)
  • 코드 값은 한글로 적는다(ApplicantStatuslabel 추가). 번호·점수는 숫자 칸, 없는 값은 빈 칸
  • 글자는 글자 칸으로만 써서 = 로 시작하는 값도 수식으로 실행되지 않는다. 쓰는 곳이 없어진 CSV 변환 함수(toCsvField)와 테스트는 지웠다

필터 유실 수정

  • ExportService.createExportJobPersistenceAdapter.save 의 반환값으로 처리 이벤트를 보낸다. export_job 에 필터 컬럼이 없어 엔티티를 거친 반환값의 필터가 비었고, 처리기는 그 필터로 지원자를 골랐다
  • 저장소가 넘겨받은 필터를 반환값에 다시 담는다. DB 에는 여전히 남기지 않는다 (재처리가 필요해지면 컬럼을 추가하도록 ponytail: 주석)

필터 조건 확장

  • 목록 조회(GET /api/v11/admin/applicants)와 같은 keyword, regions, graduationStatuses, isSubmitted 를 추가했다. 화면에서 거른 목록을 그대로 내보낼 수 있다
  • 목록 필드에 null 을 보내면 400 이던 것을 필드 없음·빈 목록과 같게 받는다

커밋

  1. fix(admin): 필터 유실 수정 + 재현 테스트
  2. feat(admin): xlsx 생성 (XlsxRenderPort, PoiXlsxAdapter, 처리기, 객체 키 .xlsx, 상태 한글 표기, CSV 함수 삭제)
  3. feat(admin): 필터 조건 확장
  4. fix(admin): 목록 필터 null 허용
  5. docs(documents): API 명세

Testing

  • Unit tests — bazel test //systems/admin/... 12개 타깃 통과
    • export_job_persistence_test (신규): DB 없이 save 만 흉내 낸 저장소로 반환값의 필터가 유지되는지 확인. 수정 전 코드에서 statuses=[FIRST_PASS][] 로 바뀌어 실패하는 것을 먼저 봤다
    • xlsx_render_test (신규): 숫자 칸·빈 칸, =HYPERLINK(...) 가 수식이 아닌 글자 칸으로 남는지 확인
  • Integration tests
  • Manual verification — 빈 MySQL 8.4 와 MinIO 에 admin 을 운영과 같은 기본 프로파일(ddl-auto: validate)로 띄워 로컬 E2E (지원자 5명을 직접 넣음)
    • 필터 없음 → 202, COMPLETED, 서명된 링크 200 (xlsx Content-Type). 시트 이름, 머리글 15열, 틀 고정, 한글·숫자·빈 칸 확인
    • regions=[DAEJEON], statuses=[FIRST_PASS], isSubmitted=true → 조건에 맞는 2명만
    • keyword=영희 + graduationStatuses + admissionTypes → 1명만
    • 필터 필드 모두 null → 전체. 공백 keyword + 빈 statuses → 전체
    • ADMISSION_TICKET + 필터 → zip 에 조건에 맞는 PDF 2개만
    • 알 수 없는 enum, type 누락, "isSubmitted": "yes" → 400. 헤더 없음 401, STUDENT 역할 403, 없는 작업 404
    • MinIO 중지 → 작업 FAILED, downloadUrl null
    • 서명된 링크에 Authorization 헤더를 붙이면 저장소가 400 으로 거절한다 → 명세에 프론트 주의사항으로 적었다

Deployment Notes

  • Feature flag: 없음
  • Migration required: 없음
  • Rollout considerations:
    • 설정 추가 없음. 기존 STORAGE_BUCKET, DOWNLOAD_URL_EXPIRES_SECONDS, AWS 표준 환경변수를 그대로 쓴다
    • admin 이미지에 POI 와 전이 의존성(xmlbeans 등)이 들어가 크기가 늘어난다
    • 필터를 보내던 클라이언트는 이제 실제로 걸러진 결과를 받는다

Checklist

  • Matches product/tech requirements
  • Backward compatibility considered — 요청은 필드 추가만. APPLICANT_LIST 결과 형식(CSV → xlsx)과 필터 적용 여부는 바뀐다
  • Docs updated if applicable — api-spec.md

리뷰어 확인 부탁:

  1. admin 지원자 테이블(admin_db.applicant)을 채우는 코드가 develop 에 없다. 목록 조회와 마찬가지로 운영 엑셀에는 머리글만 나올 수 있다. 이를 application gRPC 조회로 바꾼 fix(admin,application,identity): 데이터 소유권을 소유 시스템으로 되돌리고 끊긴 경로 연결 #145fix(notification): 공지 등록을 notification 소유로 옮겨 조회 누락 수정 #139 #143 브랜치로 병합됐는데 fix(notification): 공지 등록을 notification 소유로 옮겨 조회 누락 수정 #139 #143 이 닫혀 develop 에 들어오지 않았다. 이 PR 범위 밖이라 따로 다뤄야 한다
  2. 열에 연락처·생년월일을 넣었다. 링크는 15분 뒤 만료되지만 파일은 저장소에 남는다. 빼야 하면 ExportJobProcessorAPPLICANT_LIST_COLUMNS 한 곳만 고치면 된다
  3. 동기 다운로드 API 를 따로 만들지 않고 기존 비동기 내보내기를 썼다. 프론트는 COMPLETED 까지 상태 조회를 반복해야 한다
  4. 문서는 관리자 공지 수정·삭제 API 추가 (notification 에 gRPC 로 위임) #161 과 같은 방식으로 documents/features/applicant-list-excel/ 에 두었다
  5. Notion API 명세 DB 에는 아직 반영하지 않았다

🤖 Generated with Claude Code

tlgms and others added 5 commits September 14, 2026 22:31
export_job 에 필터 컬럼이 없어 저장 결과의 필터가 비었고, 처리기는 그 값으로
지원자를 골라 필터와 상관없이 전체를 내보냈다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 도메인에 XlsxRenderPort 를 두고 adapter-out 에서 Apache POI 로 구현한다
- 열에 생년월일·연락처·출신학교·항목별 점수를 더하고 상태를 한글로 적는다
- 객체 키 확장자를 .xlsx 로 바꾸고 쓰지 않게 된 CSV 변환 함수를 지운다

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
keyword, regions, graduationStatuses, isSubmitted 를 받아 화면에서 거른 목록을 그대로
내보낼 수 있게 한다. 추가 필드라 기존 요청은 그대로 동작한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
목록 필드가 기본값만 있는 non-null 이라 null 을 보내면 400 이 났다. 필드를 빼거나 빈 목록을
보낸 것과 같게 받는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tlgms tlgms added the ready-for-review CodeRabbit 자동 리뷰 대상 (.coderabbit.yaml) label Sep 14, 2026
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 6f81abc8-1240-4419-85b3-a71fd3ec0de5

📥 Commits

Reviewing files that changed from the base of the PR and between 97ba821 and 2a5147c.

📒 Files selected for processing (14)
  • documents/features/applicant-list-excel/api-spec.md
  • systems/admin/admin-adapter-in/src/main/kotlin/hs/kr/entrydsm/admin/adapterin/web/SupportController.kt
  • systems/admin/admin-adapter-in/src/main/kotlin/hs/kr/entrydsm/admin/adapterin/web/dto/request/AdminRequests.kt
  • systems/admin/admin-adapter-out/BUILD.bazel
  • systems/admin/admin-adapter-out/deps.bzl
  • systems/admin/admin-adapter-out/src/main/kotlin/hs/kr/entrydsm/admin/adapterout/document/PoiXlsxAdapter.kt
  • systems/admin/admin-adapter-out/src/main/kotlin/hs/kr/entrydsm/admin/adapterout/persistence/SupportPersistenceAdapters.kt
  • systems/admin/admin-adapter-out/src/test/kotlin/hs/kr/entrydsm/admin/adapterout/document/PoiXlsxAdapterTest.kt
  • systems/admin/admin-adapter-out/src/test/kotlin/hs/kr/entrydsm/admin/adapterout/persistence/ExportJobPersistenceAdapterTest.kt
  • systems/admin/admin-application/src/main/kotlin/hs/kr/entrydsm/admin/application/ExportJobProcessor.kt
  • systems/admin/admin-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt
  • systems/admin/admin-domain/src/main/kotlin/hs/kr/entrydsm/admin/domain/document/DocumentNaming.kt
  • systems/admin/admin-domain/src/main/kotlin/hs/kr/entrydsm/admin/domain/enum/ApplicantStatus.kt
  • systems/admin/admin-domain/src/main/kotlin/hs/kr/entrydsm/admin/domain/port/out/XlsxRenderPort.kt

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.

@tlgms
tlgms merged commit c634387 into develop Sep 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit 자동 리뷰 대상 (.coderabbit.yaml)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

관리자 지원자 목록 엑셀(xlsx) 다운로드 (내보내기 APPLICANT_LIST 를 CSV 에서 xlsx 로)

2 participants