Skip to content

FEAT : 찜한 병원 목록과 프로필 이미지 업로드 추가 - #98

Merged
RosieOh merged 2 commits into
mainfrom
feat/liked-hospitals-and-profile-image
Aug 30, 2026
Merged

FEAT : 찜한 병원 목록과 프로필 이미지 업로드 추가#98
RosieOh merged 2 commits into
mainfrom
feat/liked-hospitals-and-profile-image

Conversation

@RosieOh

Copy link
Copy Markdown
Contributor

개요

프런트에서 만들 수 없던 화면 두 개가 API 가 없어서 막혀 있었습니다. 그 API 를 추가하고,
붙이는 과정에서 드러난 기존 버그를 함께 고쳤습니다.

추가한 엔드포인트

메서드경로설명
GET/health/hospitals/likes찜한 병원 목록
POST/users/me/profile-image프로필 이미지 multipart 업로드
  • 찜 목록은 병원을 JOIN FETCH 로 함께 가져와 목록 길이만큼 추가 조회가 나가지 않게 했습니다.
  • 업로드는 건강기록 첨부와 같은 FileStorageService 를 씁니다. S3 로 전환할 때 함께 옮겨갑니다.
  • 기존 PUT /users/me/profile-image 는 이미지 URL 문자열만 받아서 클라이언트가 파일을
    올릴 곳이 없었습니다. 그 경로는 그대로 두고 업로드 경로를 더했습니다.

함께 고친 기존 버그

① 찜과 병원 리뷰가 전부 user_id = NULL 로 저장되고 있었습니다

HospitalLike · HospitalReviewuserIdinsertable = false 인 읽기 전용 그림자
필드입니다. 생성할 때 .userId(...) 로만 채우고 있어서 user_id 컬럼에는 아무것도 쓰이지
않았습니다.

@JoinColumn(name = "user_id", nullable = false)
privateUseruser; // ← 이걸 채워야 한다@Column(name = "user_id", insertable = false, updatable = false)
privateLonguserId; // ← 여기에 넣어도 무시된다

그 결과:

  • existsByHospitalIdAndUserId 가 항상 false → 같은 병원을 몇 번이든 찜할 수 있었음
  • isLikedByUser 가 항상 false → 찜해도 하트가 채워지지 않음
  • 병원 리뷰의 작성자를 알 수 없어 본인 리뷰 수정·삭제를 붙일 수 없었음

연관(user)을 채우도록 고쳤습니다. 로컬에서 user_id 가 실제로 들어가는 것과
like-statusliked: true 로 바뀌는 것을 확인했습니다.

② 찜 해제가 항상 500 이었습니다

HealthFacade 에 트랜잭션이 없어 파생 delete 가 죽고 있었습니다.

InvalidDataAccessApiUsageException:
No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call

save()SimpleJpaRepository 가 스스로 트랜잭션을 열어 동작했지만 deleteBy...
그렇지 않습니다. 쓰기 메서드에 @Transactional 을 붙였습니다.

보안 판단 하나

/files/profile-images/** 인증 없이 엽니다.

<img src> 에는 Authorization 헤더를 붙일 수 없어 프로필 사진이 401 로 안 보입니다.
파일명이 UUID 라 주소를 모르면 찾을 수 없고, 원래 화면에 노출되는 값입니다.

업로드 루트(/files/**) 전체를 열지 않았습니다. 같은 저장소에 건강기록 첨부가 들어 있는데
그건 민감정보라 주소만 알면 열리는 상태로 두면 안 됩니다. 그쪽은 인증을 거치는 별도 다운로드
경로가 필요하고, 이 PR 범위 밖으로 두었습니다. (현재는 401 이라 첨부 미리보기가 동작하지
않는 상태이며, 이 PR 이 그 상태를 바꾸지는 않습니다.)

검증

./gradlew test BUILD SUCCESSFUL
./gradlew bootJar BUILD SUCCESSFUL

로컬에 백엔드·MariaDB 를 띄우고 실제로 확인했습니다.

POST /health/hospitals/1/like 200 → DB user_id = 2 (기존에는 NULL)
GET /health/hospitals/likes 200 → 병원 2건
GET /health/hospitals/1/like-status 200 → liked: true (기존에는 항상 false)
DELETE /health/hospitals/1/like 200 → liked: false (기존에는 500)
POST /users/me/profile-image 200 → {"profileImageUrl":"/files/profile-images/..."}
GET /health/hospitals/1 404 → /hospitals/likes 와 경로 충돌 없음

프런트에서도 찜 목록 화면이 뜨고 프로필 사진이 실제로 렌더되는 것까지 확인했습니다.

관련 PR

프런트: CareCode-Repo/CareCode_FE#86 — 이 PR 이 먼저 머지·배포돼야 두 화면이 동작합니다.

프런트에서 만들 수 없던 화면 두 개가 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 라 주소를 모르면 찾을 수 없다. 업로드 루트 전체를 열지 않은 것은 같은
저장소에 건강기록 첨부(민감정보)가 들어 있기 때문이다 — 그쪽은 인증을 거치는 별도
다운로드 경로가 필요하다.
RosieOh added a commit to CareCode-Repo/CareCode_FE that referenced this pull request Aug 26, 2026
백엔드에 API 가 없어 못 만들던 두 가지다.
(백엔드: CareCode-Repo/CareCode_Interface#98 — 먼저 배포돼야 동작한다)
- /mypage/liked-hospitals — 찜한 병원 목록. 병원 상세에서 찜은 걸 수 있는데
모아 볼 곳이 없었다. 마이페이지에서 들어간다
- 프로필 사진 업로드. EditProfileImage 는 파일을 base64 로 바꿔 넘기고 있었지만
그걸 받아 줄 경로가 없어 어디에도 저장되지 않았고, 그래서 어느 화면에서도
쓰이지 않는 컴포넌트였다. 파일을 그대로 올리고 서버가 준 주소를 화면에 반영한다
업로드 응답의 `/files/...` 는 **백엔드 오리진** 기준 경로라 그대로 쓰면 프런트 주소로
해석돼 404 가 난다. toAbsoluteFileUrl 로 붙여 준다. next/image 는 unoptimized 로 둔다 —
외부 호스트를 최적화 대상에 넣으려면 remotePatterns 설정이 필요한데, 저장소가 S3 로
바뀌면 그 설정도 함께 바뀌어야 해서 지금 묶어 두지 않는다.
이미지·용량 검증은 클라이언트에서 먼저 거른다(서버 multipart 상한 10MB 와 맞춤).
app.storage.local.root 기본값이 ./uploads 라, 로컬에서 업로드를 시험할 때마다
저장소 안으로 파일이 쌓인다. .gitignore 에 규칙이 없어서 실제로 시험용 파일
하나가 커밋에 섞여 들어왔다(uploads/profile-images/.../*.png, 70바이트).
이번 것은 더미 파일이라 실제 피해는 없다. 문제는 경로다. 프로필 이미지와
건강기록 첨부(진단서·검진표)가 같은 디렉터리 아래 쌓이고, 이 저장소는 공개다.
한 번이라도 진짜 파일이 섞이면 커밋 이력에 남아 되돌리기 어렵다.
파일을 추적에서 빼고 uploads/ 를 무시 목록에 넣는다.
@RosieOh
RosieOh merged commit a17e918 into mainAug 30, 2026
7 checks passed
Sign up for freeto 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.

1 participant

@RosieOh