Repository navigation
feat/materials-put - #104
Merged
Merged
Conversation
- 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
reviewed
Oct 17, 2026
danjee2009
left a comment
Member
There was a problem hiding this comment.
확인된 문제
- [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·인증 서버·분석 서버 연동은 이 환경에서 실행하지 않았습니다.
- 쪽수를 세야 하는 옛 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
added a commit
that referenced
this pull request
Oct 19, 2026
fix: #104 자료 저장 API를 main에 반영 (병합 경로 누락)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
마이페이지가 직무·문서·GitHub·보완 입력을 한 번에 저장하는
PUT /api/v1/metaData/materials를 추가합니다(비개발 직군 명세 01-overview §4.1 계약 C-1). #103(GET) 위에 쌓은 PR이라 기준 브랜치가feat/materials-api입니다. #103이 병합되면 기준을main으로 바꿔 주세요.관련 이슈
별도 연결 이슈 없음. 인증 서버 쪽 저장 API는 Alpha-origin/Repit-auth-server#34입니다(먼저 병합 필요).
Task
multipart/form-data—payload(JSON) + 새로 첨부한 PDFfile0~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)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: nullGlobalExceptionHandler에MaterialValidationException→ 422 처리 추가dataUpload,getMetaData,isGit,isPortfolio,GET /materials)는 바꾸지 않음. 요청 로그는 기존 필터대로 multipart 본문을 남기지 않고 파일 이름·크기만 남김[가정]
검증·판정
JOB_ROLE_REQUIRED입니다. 직무를 알 수 없으면 나머지 규칙은 보지 않고 바로 422입니다.NO_DOCUMENT, 6개 이상은TOO_MANY_DOCUMENTS이고 문서별 검사는 하지 않습니다.NOT_PDF: 파일 이름이.pdf도 아니고 Content-Type이application/pdf도 아닐 때. 내용이 PDF가 아니면 PDFBox에서BROKEN_PDFENCRYPTED_PDF: 열 때 암호가 필요한 PDF만. 편집 제한만 걸린 PDF는 분석 서버도 읽을 수 있어 받습니다.ClassCastException등)를 던지는 PDF도BROKEN_PDF입니다(리뷰 반영).FILE_TOO_LARGE: 파일 하나 50MB 초과(A-9). 크기만 보고 내용은 읽지 않습니다. 새 파일 합계가 200MB(A-9)를 넘으면 하나도 읽지 않고documents위치에FILE_TOO_LARGE(detail에 합계 바이트) — 06 §3.4에 합계 사유가 없어 같은 사유를 씁니다(리뷰 반영).DUPLICATE_FILE: 새 파일끼리는 내용 SHA-256, 저장된 문서끼리는fileUrl로 비교합니다. 새 파일과 저장된 문서는 비교하지 않습니다(저장된 파일을 받아 와야 해서).TOO_MANY_PAGES: 모든 문서를 읽었을 때만 합계를 보고,detail에 합계 쪽수를 담습니다.REQUIRED_DOC_MISSING: 요청한 문서 종류 기준으로 판정하고detail에 필수 종류 코드(쉼표 구분)를 담습니다.MATERIALS_RESET_REQUIRED): 저장된 직무에서 새 직무로 자료를 유지할 수 없는데(개발 3직무 사이가 아님)EXISTING문서를 보내면 그 행에 답니다. GitHub·보완 입력은 값이라 서버가 "유지한 것"을 구분할 수 없어 검사하지 않습니다.pageCount: null)는 저장할 때 S3에서 받아 셉니다. 받기 전에 S3 크기를 보고 50MB를 넘으면 받지 않고 그 행에FILE_TOO_LARGE(옛 업로드 경로는 1GB까지 받았음, 리뷰 반영). 이 버킷 주소가 아니거나 받지 못하면EXISTING_NOT_FOUND입니다.EXISTING문서의 종류는 바꿔 저장할 수 있습니다(같은 파일, 다른 종류).INVALID_GITHUB_URL):github.com의 저장소 또는 계정 주소만 받습니다(https://·www.생략 가능). 다른 사이트 주소와 아이디만 적은 값은 받지 않습니다. 예전 Web은 "아이디 입력"도 받았으므로, 아이디만 저장한 사용자는 다시 저장할 때 이 사유가 납니다. 앞뒤 공백은 지웁니다. 같은 저장소를 가리키는 주소(대소문자·https·www·끝의/·.git만 다름)는 처음 것 하나로 합치고, 서로 다른 주소는 10개까지(넘으면gitUrls에detail: "TOO_MANY").settings·orgs같은 GitHub 기능 경로는 계정 자리에 오면 거절합니다(리뷰 반영).documentIndex는SUPPLEMENT_INVALID, 4개 이상은supplements위치에SUPPLEMENT_INVALID입니다.source가NEW/EXISTING이 아님, 문서 항목이 null,fileKey가file0~file4가 아님(multipart에 섞여 오는payload파트를 PDF로 읽지 않게), 문서 목록이 가리키지 않는 파일 파트가 있음(리뷰 반영).저장·분석
/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 병합과 함께 후속으로 바꿉니다.{"analysis": {"jobId": null, "status": "failed"}}를 돌려줍니다. 계약 예시에는accepted만 있어failed·skipped는 새 값입니다. 아래 "Web 후속" 참고.abort()합니다.PUT호출은 같은 본문을 다시 보내도 결과가 같은 교체라 GET처럼 5xx·연결 실패를 재시도합니다.spring.servlet.multipart제한(1024MB)은 면접 전체 녹화가 최대 1GB(InterviewRecordingService.MAX_FULL_INTERVIEW_BYTES)라 낮출 수 없습니다.JobRole의 위치(domain/metadata/support)와 모양은 03 §2를 받기 전 임시입니다. 일반 직군 확장 때 대분류 조회를 이 자리에 더합니다(01 §8.2).검증
MaterialsValidatorTest28개(임시 파일 정리, GitHub 중복 합치기·10개 상한·기능 경로, 합계 200MB 초과 시 파일을 읽지 않음, 큰 옛 PDF, 미참조 파트 400 포함): 06 §3.4 사유별, 직무 변경(QA → 보안 초기화, FE → BE 유지), 옛 사용자(백엔드 간주·쪽수 세기·비개발 전환), GitHub 필수·숨김·형식, 보완 입력, 사유 모아서 돌려주기, 400 경우. PDF는 테스트에서 실제로 만들어 PDFBox로 엶(6쪽 + 46쪽 →TOO_MANY_PAGES"52", 암호 PDF →ENCRYPTED_PDF)JobRoleTest7개: 01 §3.1 표와 같은지, 지금 분석 요청 모양으로 분석하는 직무 판단AnalysisLaunchServiceTest+2개: 비개발이면launch422, 면접 시작 자동 분석 안 함MetaDataControllerTest+1개: 비개발이면dataUpload가 저장 전에 422MaterialFileStorageTest5개: 저장된 PDF는 S3 크기를 먼저 보고 한도를 넘으면 받지 않음, 받는 도중 커지면 끊음, 업로드는 파일에서 흘려보냄PdfInspectorTest3개: PDFBox 런타임 예외도 손상으로MaterialsServiceTest11개: 같은 내용, 개발 직무 간 변경, 임시 파일에서 업로드·저장 순서·분석 요청(임시 파일 정리), 비개발skipped, 저장 실패 시 파일 정리(4xx·5xx·타임아웃이지만 저장됨·재조회 실패·인증 서버 호출 전 실패), 검사 실패 시 업로드 없음, 분석 실패 시failedMetaDataControllerMaterialsPutTest3개: multipart(payload + file0) 바인딩, 200·analysis: null·422 응답 모양AuthServerMaterialsWiringTest+1개: 가짜 인증 서버로PUT메서드·토큰·JSON 본문./gradlew test: 전체 804개 통과, 실패·오류·건너뜀 0개git diff --check통과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