Skip to content

feat/materials-put - #104

Merged
kimwuju merged 5 commits into
feat/materials-apifrom
feat/materials-put
Oct 17, 2026
Merged

kimwuju merged 5 commits into
feat/materials-apifrom
feat/materials-put

Conversation

@kimwuju

@kimwuju kimwuju commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

마이페이지가 직무·문서·GitHub·보완 입력을 한 번에 저장하는 PUT /api/v1/metaData/materials를 추가합니다(비개발 직군 명세 01-overview §4.1 계약 C-1). #103(GET) 위에 쌓은 PR이라 기준 브랜치가 feat/materials-api입니다. #103이 병합되면 기준을 main으로 바꿔 주세요.

비개발 API 명세(03-api.md) 없이 구현했습니다. 01-overview(규칙·가정 A-7~A-10), 06-web §3.3·§3.4(사유 목록·문구), 상세 기획 §4, 일반 직군 03-api의 검증 순서 추정을 근거로 정했고, 명세가 정해야 할 판단은 아래 **[가정]**에 모두 적었습니다. 03-api와 다른 곳이 있으면 그쪽에 맞추겠습니다.

관련 이슈

별도 연결 이슈 없음. 인증 서버 쪽 저장 API는 Alpha-origin/Repit-auth-server#34입니다(먼저 병합 필요).

Task

  • 요청: multipart/form-data — payload(JSON) + 새로 첨부한 PDF file0~file4
    {"jobRole": "PRODUCT_MANAGER",
     "documents": [{"type": "PRD", "source": "NEW", "fileKey": "file0"},
                   {"type": "POLICY_DOC", "source": "EXISTING", "fileUrl": "https://.../c3d4.pdf"}],
     "gitUrls": [],
     "supplements": [{"title": "회원가입 개편", "documentIndex": 0, "goal": "...", "role": "...", "decision": "...", "outcome": "..."}]}
  • 검증(MaterialsValidator): 직무 → 문서(직무 변경, 종류, 파일, 중복, 쪽수 합계, 필수 종류) → GitHub → 보완 입력. 걸린 규칙은 모아서 한 번에 돌려줌
    • 직무 명세 JobRole: 01 §3.1 표의 9개(GitHub 필수·선택·숨김, 필수·선택 문서, 보완 입력 여부, 개발 3직무 사이만 자료 유지)
    • PdfInspector(PDFBox 3.0.4): 쪽수, 손상·암호화 판정. 쪽수는 서버만 셈(A-8)
  • 422 MaterialErrorResponse: {"message": "자료를 저장하지 못했습니다.", "errors": [{"field": "documents[1]", "reason": "ENCRYPTED_PDF", "detail": null}]}. reason은 06 §3.4 표와 같은 18개
  • 같은 내용이면 저장·분석 없이 {"materials": …, "analysis": null}(A-10)
  • 저장(MaterialsService): 검사를 모두 통과한 뒤 새 파일을 S3에 올리고 인증 서버 PUT /api/v1/auth/materials(C-2)로 저장. 인증 서버 저장이 실패하면 방금 올린 파일을 지우고 실패를 그대로 응답
  • 분석 요청: 저장 뒤 개발 직무는 AnalysisLaunchService.launch로 요청하고 {"analysis": {"jobId": "...", "status": "accepted"}}. 요청이 실패하면 {"jobId": null, "status": "failed"}, 비개발 직무는 요청하지 않고 {"jobId": null, "status": "skipped"}. 분석 재료가 바뀌지 않았으면 analysis: null
  • GlobalExceptionHandler에 MaterialValidationException → 422 처리 추가
  • 기존 엔드포인트(dataUpload, getMetaData, isGit, isPortfolio, GET /materials)는 바꾸지 않음. 요청 로그는 기존 필터대로 multipart 본문을 남기지 않고 파일 이름·크기만 남김

[가정]

검증·판정

  1. 검증 순서와 사유별 판정 기준은 위 근거로 정했습니다(03 §3.2 미확인).
  2. 직무가 없거나 모르는 코드도 JOB_ROLE_REQUIRED입니다. 직무를 알 수 없으면 나머지 규칙은 보지 않고 바로 422입니다.
  3. 문서 0개는 NO_DOCUMENT, 6개 이상은 TOO_MANY_DOCUMENTS이고 문서별 검사는 하지 않습니다.
  4. NOT_PDF: 파일 이름이 .pdf도 아니고 Content-Type이 application/pdf도 아닐 때. 내용이 PDF가 아니면 PDFBox에서 BROKEN_PDF
  5. ENCRYPTED_PDF: 열 때 암호가 필요한 PDF만. 편집 제한만 걸린 PDF는 분석 서버도 읽을 수 있어 받습니다.
  6. 쪽이 0개인 PDF와 PDFBox가 런타임 예외(ClassCastException 등)를 던지는 PDF도 BROKEN_PDF입니다(리뷰 반영).
  7. FILE_TOO_LARGE: 파일 하나 50MB 초과(A-9). 크기만 보고 내용은 읽지 않습니다. 새 파일 합계가 200MB(A-9)를 넘으면 하나도 읽지 않고 documents 위치에 FILE_TOO_LARGE(detail에 합계 바이트) — 06 §3.4에 합계 사유가 없어 같은 사유를 씁니다(리뷰 반영).
  8. DUPLICATE_FILE: 새 파일끼리는 내용 SHA-256, 저장된 문서끼리는 fileUrl로 비교합니다. 새 파일과 저장된 문서는 비교하지 않습니다(저장된 파일을 받아 와야 해서).
  9. TOO_MANY_PAGES: 모든 문서를 읽었을 때만 합계를 보고, detail에 합계 쪽수를 담습니다.
  10. REQUIRED_DOC_MISSING: 요청한 문서 종류 기준으로 판정하고 detail에 필수 종류 코드(쉼표 구분)를 담습니다.
  11. 직무 변경(MATERIALS_RESET_REQUIRED): 저장된 직무에서 새 직무로 자료를 유지할 수 없는데(개발 3직무 사이가 아님) EXISTING 문서를 보내면 그 행에 답니다. GitHub·보완 입력은 값이라 서버가 "유지한 것"을 구분할 수 없어 검사하지 않습니다.
  12. 직무가 없는 옛 사용자는 백엔드로 보고 유지 여부를 판단합니다(A-7). 그래서 개발 직무로는 옛 PDF를 그대로 쓰고, 비개발로 바꾸면 초기화가 필요합니다.
  13. 쪽수를 센 적 없는 옛 PDF(pageCount: null)는 저장할 때 S3에서 받아 셉니다. 받기 전에 S3 크기를 보고 50MB를 넘으면 받지 않고 그 행에 FILE_TOO_LARGE(옛 업로드 경로는 1GB까지 받았음, 리뷰 반영). 이 버킷 주소가 아니거나 받지 못하면 EXISTING_NOT_FOUND입니다.
  14. EXISTING 문서의 종류는 바꿔 저장할 수 있습니다(같은 파일, 다른 종류).
  15. GitHub 형식(INVALID_GITHUB_URL): github.com의 저장소 또는 계정 주소만 받습니다(https://·www. 생략 가능). 다른 사이트 주소와 아이디만 적은 값은 받지 않습니다. 예전 Web은 "아이디 입력"도 받았으므로, 아이디만 저장한 사용자는 다시 저장할 때 이 사유가 납니다. 앞뒤 공백은 지웁니다. 같은 저장소를 가리키는 주소(대소문자·https·www·끝의 /·.git만 다름)는 처음 것 하나로 합치고, 서로 다른 주소는 10개까지(넘으면 gitUrls에 detail: "TOO_MANY"). settings·orgs 같은 GitHub 기능 경로는 계정 자리에 오면 거절합니다(리뷰 반영).
  16. 보완 입력: 내용이 하나도 없는 프로젝트(화면에 펼쳐만 둔 빈 칸)는 오류 없이 빼고 저장합니다. 제목 80자·항목 500자 초과, 문서 범위를 벗어난 documentIndex는 SUPPLEMENT_INVALID, 4개 이상은 supplements 위치에 SUPPLEMENT_INVALID입니다.
  17. 형식이 잘못된 요청은 422가 아니라 400입니다: 없는 파일 파트를 가리킴, source가 NEW/EXISTING이 아님, 문서 항목이 null, fileKey가 file0~file4가 아님(multipart에 섞여 오는 payload 파트를 PDF로 읽지 않게), 문서 목록이 가리키지 않는 파일 파트가 있음(리뷰 반영).

저장·분석

  1. 같은 내용 판정: 직무, 문서(종류·주소·순서), GitHub(순서 포함), 보완 입력이 모두 같고 새 파일이 없을 때만 같은 내용입니다. 새 파일이 있으면 내용이 같아도 바뀐 것으로 봅니다.
  2. 분석 요청 모양: 분석 서버의 새 /profile 요청(C-3: jobRole·documents·supplements)이 붙기 전까지 개발 직무는 **지금 요청 모양(첫 문서 주소 + GitHub)**으로 보냅니다. 비개발 직무는 그 모양으로는 맞는 분석이 나오지 않아 분석하지 않습니다. 판단은 JobRole.supportsCurrentProfileRequest 하나로 모았고 분석을 시작하는 네 길이 모두 씁니다: PUT은 status: "skipped", 면접 시작의 launchIfMissing은 건너뜀, POST /api/v1/ai/generate·POST /dataUpload는 422 "비개발 직무는 아직 자료 분석을 지원하지 않습니다."(dataUpload는 자료를 저장하기 전에 막음). 직무가 없는 옛 사용자는 백엔드로 봅니다(A-7). analysis: null은 Web에서 "변경된 내용이 없어요"로 읽혀 skipped를 따로 뒀습니다(리뷰 반영). C-3은 Analysis 병합과 함께 후속으로 바꿉니다.
  3. 분석 요청 실패: 저장은 이미 끝났으므로 되돌리지 않고 200 + {"analysis": {"jobId": null, "status": "failed"}}를 돌려줍니다. 계약 예시에는 accepted만 있어 failed·skipped는 새 값입니다. 아래 "Web 후속" 참고.
  4. 교체된 옛 S3 파일은 지우지 않습니다(옛 업로드 경로와 같음). 인증 서버 저장이 실패처럼 보일 때(리뷰 반영): 인증 서버를 부르기 전 실패나 4xx면 저장되지 않은 게 확실해 새 파일을 지웁니다. 5xx·타임아웃은 커밋하고 응답만 늦었을 수 있어 다시 조회합니다. 보낸 내용(직무·문서·GitHub·보완 입력)이 그대로 저장돼 있으면 성공으로 보고 분석 요청까지 이어 갑니다(사용자가 실패로 보고 다시 올리지 않게). 다르면 저장된 문서가 가리키지 않는 새 파일만 지우고 실패를 돌려주며, 다시 조회도 실패하면 아무것도 지우지 않습니다. S3에서 받는 도중 상한을 넘으면 닫기 전에 abort()합니다.
  5. 인증 서버 PUT 호출은 같은 본문을 다시 보내도 결과가 같은 교체라 GET처럼 5xx·연결 실패를 재시도합니다.
  6. 업로드 크기·메모리: 파일당 50MB와 새 파일 합계 200MB(A-9)는 파일을 옮기기 전에 막습니다. 새 파일은 임시 파일로 옮겨 해시·PDFBox·S3 업로드를 모두 파일·스트림으로 처리하고, 옛 PDF도 임시 파일로 받아(받는 동안 50MB에서 끊음) 힙에 PDF를 통째로 올리지 않습니다(리뷰 반영). 임시 파일은 요청이 끝나면 지웁니다. 전역 spring.servlet.multipart 제한(1024MB)은 면접 전체 녹화가 최대 1GB(InterviewRecordingService.MAX_FULL_INTERVIEW_BYTES)라 낮출 수 없습니다.
  7. 직무 명세 JobRole의 위치(domain/metadata/support)와 모양은 03 §2를 받기 전 임시입니다. 일반 직군 확장 때 대분류 조회를 이 자리에 더합니다(01 §8.2).

검증

  • 새 테스트 61개
    • MaterialsValidatorTest 28개(임시 파일 정리, GitHub 중복 합치기·10개 상한·기능 경로, 합계 200MB 초과 시 파일을 읽지 않음, 큰 옛 PDF, 미참조 파트 400 포함): 06 §3.4 사유별, 직무 변경(QA → 보안 초기화, FE → BE 유지), 옛 사용자(백엔드 간주·쪽수 세기·비개발 전환), GitHub 필수·숨김·형식, 보완 입력, 사유 모아서 돌려주기, 400 경우. PDF는 테스트에서 실제로 만들어 PDFBox로 엶(6쪽 + 46쪽 → TOO_MANY_PAGES "52", 암호 PDF → ENCRYPTED_PDF)
    • JobRoleTest 7개: 01 §3.1 표와 같은지, 지금 분석 요청 모양으로 분석하는 직무 판단
    • AnalysisLaunchServiceTest +2개: 비개발이면 launch 422, 면접 시작 자동 분석 안 함
    • MetaDataControllerTest +1개: 비개발이면 dataUpload가 저장 전에 422
    • MaterialFileStorageTest 5개: 저장된 PDF는 S3 크기를 먼저 보고 한도를 넘으면 받지 않음, 받는 도중 커지면 끊음, 업로드는 파일에서 흘려보냄
    • PdfInspectorTest 3개: PDFBox 런타임 예외도 손상으로
    • MaterialsServiceTest 11개: 같은 내용, 개발 직무 간 변경, 임시 파일에서 업로드·저장 순서·분석 요청(임시 파일 정리), 비개발 skipped, 저장 실패 시 파일 정리(4xx·5xx·타임아웃이지만 저장됨·재조회 실패·인증 서버 호출 전 실패), 검사 실패 시 업로드 없음, 분석 실패 시 failed
    • MetaDataControllerMaterialsPutTest 3개: multipart(payload + file0) 바인딩, 200·analysis: null·422 응답 모양
    • AuthServerMaterialsWiringTest +1개: 가짜 인증 서버로 PUT 메서드·토큰·JSON 본문
  • ./gradlew test: 전체 804개 통과, 실패·오류·건너뜀 0개
  • git diff --check 통과
  • 실제 인증 서버·S3·분석 서버와 맞물리는 확인은 하지 못했습니다(fix: fix question tailor #34 병합 전, 로컬 S3·분석 서버 없음)

Web 후속 (06-web §3.3에 더할 것)

  • status: "failed"면 "다시 분석"을 띄워 주세요. 면접 준비는 가장 최근의 완료된 분석을 쓰므로(QuestionPoolService.latestCompleted), 자료를 바꾼 뒤 분석 요청이 실패하면 다시 분석할 때까지 이전 자료의 분석으로 면접이 진행됩니다. 다시 분석은 지금 있는 POST /api/v1/ai/generate(useProfileStatus.reanalyze)로 할 수 있습니다(리뷰 반영)
  • status: "skipped"(비개발) 는 저장은 됐지만 아직 분석하지 않는 상태입니다. "저장했어요"와 함께 분석 준비 중 안내가 필요합니다. 비개발 직무는 Web 5단계 전까지 고를 수 없습니다
  • 비개발 직무로 저장한 사용자가 "다시 분석"(/api/v1/ai/generate)을 누르면 422입니다. C-3 전까지는 비개발 직무에서 다시 분석 버튼을 숨기거나 안내 문구로 바꿔 주세요. 면접 시작 때의 자동 분석도 비개발 직무는 돌지 않습니다

ETC

  • 의존성 추가: org.apache.pdfbox:pdfbox:3.0.4
  • 후속: 분석 서버 새 /profile 요청(C-3), 03-api.md를 받으면 [가정] 대조, Web 연결(06 §3.3)
  • main 병합 시 deploy.yml이 배포를 실행합니다

🤖 Generated with Claude Code

kimwuju and others added 2 commits October 17, 2026 16:21
- payload(JSON) + file0~file4 multipart. 저장된 문서는 EXISTING, 새 파일은 NEW
- 검증(직무 → 문서 → GitHub → 보완 입력)을 모아 422 MaterialErrorResponse로 돌려준다
  - 직무 명세 JobRole(01 §3.1), 개발 3직무 사이만 자료 유지, 직무 없는 옛 사용자는 백엔드로 본다
  - PDFBox로 쪽수를 세고 손상·암호화를 가린다. 파일당 50MB, 합계 50쪽, 같은 파일 중복 금지
- 같은 내용이면 저장·분석 없이 analysis: null
- 새 파일을 S3에 올린 뒤 인증 서버 PUT /api/v1/auth/materials로 저장하고, 실패하면 올린 파일을 지운다
- 저장 뒤 지금 /profile 요청 모양(첫 문서 + GitHub)으로 분석을 요청한다. 실패해도 저장은 유지하고 status: failed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- JobRole이 01 §3.1 표와 같은지
- 검증 규칙 20개(06 §3.4 사유별, 직무 변경, 옛 사용자, 모아서 돌려주기)
- 저장 흐름(같은 내용, 업로드·저장·분석 요청, 저장 실패 시 파일 정리, 분석 실패)
- multipart 바인딩과 200·422 응답 모양, 인증 서버 PUT 본문

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@danjee2009 danjee2009 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

확인된 문제

  • [P2] src/main/java/repit/repit_api_server/domain/metadata/service/MaterialsValidator.java:172 — 기존 PDF를 크기 제한 없이 메모리로 읽습니다. pageCount가 없는 레거시 문서는 storage.download()가 S3 객체 전체를 getObjectAsBytes()로 메모리에 올린 뒤 PDFBox에 전달합니다. 예전 업로드 경로는 multipart 파일을 최대 1GB까지 받을 수 있어, 큰 레거시 PDF를 EXISTING으로 보내면 요청 하나로 API 프로세스의 메모리가 고갈될 수 있습니다. S3 객체 크기를 읽기 전에 확인해 50MB 제한을 적용하거나 제한된 스트림으로 검사해 주세요.
  • [P2] src/main/java/repit/repit_api_server/domain/metadata/service/MaterialsValidator.java:198 — 새 파일의 전체 크기와 미참조 multipart 파일을 제한하지 않습니다. 50MB 검사는 payload가 참조한 fileKey에만 적용됩니다. 다섯 개의 새 문서는 합계 250MB까지 각각 버퍼링될 수 있고, payload에 없는 추가 파일 파트는 검사 없이 요청 파싱을 거칩니다. 현재 전역 요청 한도는 1GB라 동시 요청에서 메모리·임시 디스크와 네트워크를 과도하게 소모할 수 있습니다. 참조되지 않은 파트를 거부하고, 파일 전체 합계 제한(인계한 200MB 기준 포함)을 파일 바이트를 읽기 전에 적용해 주세요.

변경된 저장·검증 흐름과 S3 처리 코드를 확인했습니다. PR CI 테스트 통과를 확인했으며, 실제 S3·인증 서버·분석 서버 연동은 이 환경에서 실행하지 않았습니다.

kimwuju and others added 3 commits October 17, 2026 17:59
- 쪽수를 세야 하는 옛 PDF는 S3에서 크기를 먼저 보고 50MB를 넘으면 받지 않는다(FILE_TOO_LARGE)
- 새 파일 합계가 200MB(가정 A-9)를 넘으면 하나도 읽지 않고 documents에 FILE_TOO_LARGE
- 문서 목록이 가리키지 않는 파일 파트가 있으면 400

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 새 파일은 임시 파일로 옮겨 해시·PDFBox·S3 업로드를 파일로 처리한다(요청 하나가 힙에 200MB를 잡지 않게)
- 옛 PDF도 HEAD 뒤 임시 파일로 받고, 받는 동안에도 50MB에서 끊는다
- PDFBox 런타임 예외(ClassCastException 등)도 BROKEN_PDF
- 인증 서버 저장이 5xx·타임아웃이면 다시 조회해 저장된 파일은 남기고, 확인하지 못하면 지우지 않는다(4xx·호출 전 실패만 바로 지움)
- 비개발 직무는 분석 서버 새 요청 모양(C-3) 전까지 분석을 요청하지 않고 status: skipped
- GitHub 주소는 서로 다른 것 10개까지, 같은 저장소는 하나로 합치고, settings·orgs 같은 기능 경로는 거절

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- JobRole.supportsCurrentProfileRequest 하나로 판단: PUT skipped, launchIfMissing 건너뜀, /generate·/dataUpload 422(dataUpload 는 저장 전에)
- 인증 서버 응답이 실패여도 다시 조회한 값이 보낸 내용과 같으면 저장 성공으로 보고 분석 요청까지 이어 간다
- S3 에서 받는 도중 상한을 넘으면 닫기 전에 abort 해 남은 본문을 읽지 않는다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kimwuju
kimwuju merged commit dcc2a7b into feat/materials-api Oct 17, 2026
1 check passed
kimwuju added a commit that referenced this pull request Oct 19, 2026
fix: #104 자료 저장 API를 main에 반영 (병합 경로 누락)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants