Uh oh!
There was an error while loading. Please reload this page.
FEAT : 건강기록 첨부에 인증 다운로드 경로 추가 - #99
Merged
Merged
Conversation
added 2 commits
August 26, 2026 18:48
프런트에서 만들 수 없던 화면 두 개가 API 가 없어서 막혀 있었다. - GET /health/hospitals/likes — 찜한 병원 목록. 찜은 걸 수 있는데 모아 볼 방법이 없었다. 병원을 JOIN FETCH 로 함께 가져와 N+1 을 막는다 - POST /users/me/profile-image — multipart 업로드. 기존 PUT 은 이미지 URL 문자열만 받아서 클라이언트가 파일을 올릴 곳이 없었다. 건강기록 첨부와 같은 FileStorageService 를 쓰므로 S3 로 옮길 때 함께 옮겨간다 만들면서 드러난 기존 버그 두 가지를 함께 고쳤다. HospitalLike·HospitalReview 의 userId 는 insertable=false 인 읽기 전용 그림자 필드다. 생성 시 .userId(...) 로만 채우고 있어서 user_id 컬럼에는 아무것도 쓰이지 않았고, 모든 찜과 병원 리뷰가 user_id=NULL 로 저장됐다. 그 결과 중복 확인·찜 여부·작성자 판별이 전부 어긋나 있었다. 연관(user) 자체를 채우도록 고쳤다. HealthFacade 에 트랜잭션이 없어 파생 delete(deleteByHospitalIdAndUserId)가 "No EntityManager with actual transaction" 으로 죽었다. 찜 해제가 항상 500 이었다. 쓰기 메서드에 @transactional 을 붙였다. /files/profile-images/** 만 인증 없이 연다. <img src> 에는 인증 헤더를 붙일 수 없고 파일명이 UUID 라 주소를 모르면 찾을 수 없다. 업로드 루트 전체를 열지 않은 것은 같은 저장소에 건강기록 첨부(민감정보)가 들어 있기 때문이다 — 그쪽은 인증을 거치는 별도 다운로드 경로가 필요하다.
첨부는 업로드는 되는데 **한 번도 열어본 적이 없는** 상태였다. 저장 위치가
`/files/**` 인데 그 경로는 인증 뒤에 있어 401 이고, `<a href>` 나 `<img src>` 에는
Authorization 헤더를 붙일 수 없기 때문이다.
정적 경로를 열어서 해결하지 않았다. 같은 저장소에 진료 기록 사진이 들어 있어
주소만 아는 사람이 남의 건강 정보를 볼 수 있게 된다. 대신 소유권을 확인하고
서버가 직접 내려주는 경로를 만들었다.
- GET /health/records/{recordId}/attachments/{attachmentId}/download
본인 기록인지 확인한 뒤에만 본문을 스트리밍한다. 다른 기록의 첨부 id 를 끼워 넣어도
404 다. 한글 파일명은 RFC 5987 로 인코딩해 브라우저가 그대로 쓰게 한다
- FileStorageService.load(key) / toKey(publicUrl) 추가.
저장소 밖을 가리키는 키는 읽지 않는다(delete 와 같은 방어)
확인한 것:
GET /files/health-records/... 401 (정적 경로는 그대로 막힘)
GET .../attachments/1/download (인증 없음) 401
GET .../attachments/1/download (인증) 200, 원본과 바이트 동일
GET /health/records/999/attachments/1/download 404 (남의 기록)RosieOh added a commit
to CareCode-Repo/CareCode_FE
that referenced
this pull request
Aug 29, 2026
첨부 링크가 `<a href="/files/...">` 였다. 그 경로는 프런트 주소로 해석되고, 백엔드 주소로 바꿔도 인증이 필요해 401 이다. 업로드는 되는데 한 번도 열어본 적이 없었다. (백엔드: CareCode-Repo/CareCode_Interface#99 — 먼저 배포돼야 동작한다) - 인증된 axios 로 본문을 받아 blob 으로 다룬다. 파일명 클릭은 내려받기, 이미지는 object URL 로 목록에서 바로 미리보기 - object URL 은 언마운트에서 해제한다. 그러지 않으면 화면을 오갈 때마다 blob 이 쌓인다 본문 조회는 useEffect 가 아니라 쿼리로 감쌌다. 직접 받으면 같은 첨부에 요청이 렌더마다 중복돼(개발 모드에서 1개 첨부에 4번 나갔다) 캐시·중복 제거가 필요하다. 남은 2번은 StrictMode 이중 호출이라 프로덕션에는 없다.
app.storage.local.root 기본값이 ./uploads 라, 로컬에서 업로드를 시험할 때마다 저장소 안으로 파일이 쌓인다. .gitignore 에 규칙이 없어서 실제로 시험용 파일 하나가 커밋에 섞여 들어왔다(uploads/profile-images/.../*.png, 70바이트). 이번 것은 더미 파일이라 실제 피해는 없다. 문제는 경로다. 프로필 이미지와 건강기록 첨부(진단서·검진표)가 같은 디렉터리 아래 쌓이고, 이 저장소는 공개다. 한 번이라도 진짜 파일이 섞이면 커밋 이력에 남아 되돌리기 어렵다. 파일을 추적에서 빼고 uploads/ 를 무시 목록에 넣는다.
…hment-authenticated-download
응답 코드가 주석과 달랐다. IllegalArgumentException 과 BusinessException 은 전역 핸들러가 ErrorCode 와 무관하게 400 으로 바꾼다. 그래서 "없는 첨부"는 400, "남의 건강기록"은 404 로 갈렸다. 그 차이만으로 첨부 id 의 존재 여부를 알아낼 수 있다. - HealthRecordAttachmentService: download/delete 의 IllegalArgumentException 을 ResourceNotFoundException 으로. 소유권 실패와 같은 404 가 된다 - LocalFileStorageService.load: 같은 이유로 BusinessException(RESOURCE_NOT_FOUND) 를 ResourceNotFoundException 으로 시험용 업로드 파일을 추적에서 뺐다. uploads/health-records/.../*.png (70바이트) 가 커밋에 섞여 있었다. 더미 파일이라 실제 피해는 없지만, 건강기록 첨부는 진단서·검진표이고 이 저장소는 공개다. uploads/ 무시 규칙은 부모 브랜치에서 함께 들어온다. 회귀 테스트 8건. 본인 기록의 첨부는 받을 수 있다 저장 키는 공개 URL 을 되돌려 얻는다 남의 건강기록이면 404 이고 파일을 읽지 않는다 ← 소유권 확인이 저장소 접근보다 먼저 다른 기록에 달린 첨부 id 를 끼워 넣어도 받을 수 없다 없는 첨부와 남의 첨부는 같은 예외를 낸다 ← 존재 여부가 새지 않는다 건강기록이 없으면 404 다른 기록의 첨부는 지울 수 없고 404 다 387 tests, 0 failures.
Uh oh!
There was an error while loading. Please reload this page.
5 tasks
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 freeto 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.
개요
건강기록 첨부는 업로드는 되는데 한 번도 열어본 적이 없는 상태였습니다.
저장 위치가
/files/**인데 그 경로는 인증 뒤에 있어 401 이고,<a href>나<img src>에는Authorization 헤더를 붙일 수 없기 때문입니다.
정적 경로를 열어서 해결하지 않았습니다. 같은 저장소에 진료 기록 사진이 들어 있어, 열면
주소만 아는 사람이 남의 건강 정보를 볼 수 있게 됩니다. 대신 소유권을 확인하고 서버가 직접
내려주는 경로를 만들었습니다.
변경 사항
GET/health/records/{recordId}/attachments/{attachmentId}/downloadrecordId와 첨부의 소속을 대조).filename*)로 인코딩해 브라우저가 그대로 쓰게 합니다.FileStorageService에load(key)와toKey(publicUrl)을 더했습니다.저장소 밖을 가리키는 키는 읽지 않습니다 — 기존
delete와 같은 방어입니다.검증
프런트에서도 첨부 미리보기가 실제로 그려지는 것까지 확인했습니다.
관련 PR
프런트: CareCode-Repo/CareCode_FE#86 — 이 PR 이 먼저 머지·배포돼야 첨부를 열 수 있습니다.
같은 저장소를 건드리는 PR 이 하나 더 열려 있습니다: #98 (프로필 이미지 업로드).
그쪽은
/files/profile-images/**만 공개로 열고, 이 PR 은 건강기록 첨부를 공개하지 않는쪽을 택했습니다. 두 PR 의 방향이 어긋나지 않습니다 — 민감도가 다른 파일을 다르게 다룹니다.