Skip to content

feat: 담지 않고 공유 링크만 발급하는 API - #262

Merged
sevineleven merged 5 commits into
devfrom
feat/261-share-without-saving
Aug 13, 2026
Merged

feat: 담지 않고 공유 링크만 발급하는 API#262
sevineleven merged 5 commits into
devfrom
feat/261-share-without-saving

Conversation

@sevineleven

Copy link
Copy Markdown
Contributor

Situation

  • 추천 결과 화면(내 코스에 담기 전)에서 공유를 누르면 "먼저 내 코스에 담아주세요" 안내만 떴다.
  • "공유하려면 왜 먼저 담아야 하지?" 가 사용자에게 자연스럽지 않다. 담기는 여행 날짜 선택까지 거쳐야 해서, 그냥 친구에게 보여주고 싶은 사람에게는 단계가 과하다.
  • 보여주는 것과 내 계획으로 삼는 것은 다른 행동인데, 지금은 앞의 것을 하려면 뒤의 것을 강제하고 있었다.

Task

핵심 질문 하나: 담지 않는다면서 무엇을 저장하는가. 링크로 열려면 코스 데이터가 어딘가 있어야 한다. 그러면서도 "내 코스" 에는 안 보여야 한다.

어떻게 왜 채택 / 왜 아닌가
주인 없이 저장 (채택) 코스 행은 만들되 소유자를 비운다 "내 코스" 조회가 전부 게스트 범위(목록·상세·삭제)라 주인이 없으면 어느 질의에도 안 걸린다. 규칙이 한 줄로 끝난다
플래그 컬럼 + 질의마다 조건 컬럼을 더하고 목록·상세·삭제 질의에 조건을 붙인다 기존 질의 넷을 모두 고쳐야 하고, 하나라도 빠뜨리면 담지 않은 코스가 남의 목록에 뜬다
코스를 저장하지 않고 스냅샷만 별도 보관 요청 본문을 통째로 따로 적어둔다 공개 조회가 지금 코스 테이블을 읽는다. 읽는 길이 둘로 갈려 화면이 두 벌이 된다

Action

엔드포인트

  • POST /api/v1/courses/share — 요청 본문은 저장(POST /courses)과 같고, 응답은 shareToken 하나. 성공은 201.
  • X-Guest-Id 를 받지 않는다. 소유 관계를 만들지 않으므로 쓸 데가 없고, 받아 두면 "담긴다" 는 오해만 남는다.
  • 응답에 코스 내용을 싣지 않는다. 화면은 방금 보고 있던 코스를 그대로 들고 있고, 붙여 보내려면 혜택·날씨를 다시 조립해야 해서 기상청 호출이 딸려온다.
  • 구성 검증은 저장과 같은 길을 탄다. 링크로 열리는 코스가 담은 코스보다 느슨할 이유가 없다. 요청 DTO의 출발지 검사와 예외 번역을 한 곳으로 모아 두 팩토리가 함께 쓰게 했다.
  • 멱등하지 않다. 같은 본문을 두 번 보내면 링크가 두 개 생긴다. 본문만으로는 같은 코스인지 알 근거가 없어 합칠 수 없다. 담은 코스의 링크가 코스당 하나인 것과 다른 점이라 API 문서에 적었다.

남는 것 (retention)

담지 않은 코스는 쌓이기만 한다. 주인이 없어 삭제 API 로도 지울 수 없기 때문이다. 지금 배치를 만들지는 않았고, 대신 지울 수 있는 근거를 남겨 뒀다.

  • 지울 대상을 특정할 수 있다 — 주인이 없는 코스가 곧 담지 않은 코스다.
  • 나이를 알 수 있다 — 코스 테이블에는 생성 시각이 없지만, 공유 행의 발급 시각이 그 역할을 한다. 담지 않은 코스는 반드시 공유 행과 짝이라 빠지는 것이 없다.
  • 한 건의 무게: 코스 1행 + 하루 일정 최대 3행 + 슬롯 수십 행. 담아서 저장하는 코스와 같은 크기라, 저장 대비 몇 배가 되는 부담은 아니다. 운영 DB 가 EC2 도커 안의 MySQL 하나뿐이라 늘어나는 속도는 지켜봐야 한다.
  • 정리를 붙일 때의 형태: 발급 시각이 일정 기간 지난 주인 없는 코스를 지운다. 공유 행은 남겨 두면 그 링크가 404 가 아니라 410(게시자가 삭제함)로 답한다.

Result

  • 담기 전에도 공유 버튼이 동작한다. 담기는 여행 날짜 선택을 포함한 별도 행동으로 남는다.
  • 한 번 발급하면 되돌릴 수 없다(주인이 없어 삭제 API 로 못 지운다). API 문서에 "링크를 뿌리기 전에 누를 버튼" 이라고 적었다.
  • 머지 순서 주의: fix: 코스 상세·목록 응답에 shareToken 을 채운다 #260(상세·목록에 shareToken 채우기)과 같은 파일(CourseStorageApi·CourseStorageService·공유 통합 테스트)을 건드린다. fix: 코스 상세·목록 응답에 shareToken 을 채운다 #260 이 먼저 들어가면 이 브랜치에서 dev 를 한 번 머지해야 한다.
  • 검증에서 비자명한 부분: 이 PR 의 전제는 "주인이 없으면 게스트 범위 질의에 안 걸린다" 하나다. 그래서 링크가 열리는지뿐 아니라 담지 않은 코스가 내 코스 목록에 안 나오는지를 단언한다. 전제가 깨지면 거기서 걸린다.

작업을 마치기 전 자문 셋

  1. 운영에서 버티는가 — 스키마 변경도 부팅 적재 변화도 없다. 다만 위 retention 대로 행이 쌓이기만 하는 첫 경로다. 지울 근거(주인 없음 + 발급 시각)를 남겨 뒀고, 삭제 배치는 증가 속도를 보고 붙인다.
  2. 외부 API 한도 — 이 경로는 외부를 부르지 않는다. 혜택·날씨를 조립하지 않기로 한 결정이 그 이유이기도 하다. 링크를 여는 쪽에서만 기존만큼 부른다.
  3. 코스의 완성도 — 코스 구성 자체는 그대로다. 만든 코스를 남에게 보여주는 경로의 개선이다.

연관 이슈

- POST /api/v1/courses/share — 요청은 저장과 같은 payload, 응답은 shareToken 하나
- 추천 결과 화면에서 공유를 누르면 "먼저 담아주세요" 안내만 띄우고 있었다. 담기는 여행 날짜
  선택까지 거쳐야 해서, 그냥 보여주려는 사람에게는 단계가 과했다
- 링크로 열려면 코스가 어딘가 있어야 하므로 코스 자체는 영속한다. 다만 **주인 없이** 저장해
  "내 코스" 어디에도 안 나오게 했다 — 목록·상세·삭제가 전부 guest_id 로 좁히므로 주인이 없으면
  어느 질의에도 안 걸린다. 플래그 컬럼을 더하고 질의마다 조건을 붙이는 안도 있었지만, 그러면
  기존 질의 넷을 모두 고쳐야 하고 하나라도 빠뜨리면 담지 않은 코스가 남의 목록에 뜬다
- 그 대가로 이 코스는 아무도 지울 수 없다(삭제도 소유자 범위로만 돈다). 정리는 발급 시각
  (course_share.created_at)을 근거로 나중에 일괄로 한다 — 지금 배치까지 만들지는 않는다
- X-Guest-Id 를 받지 않는다. 소유 관계를 만들지 않으므로 쓸 데가 없고, 받아두면 "담긴다" 는
  오해만 남는다
- 응답에 코스 내용을 싣지 않는다. 화면이 방금 보던 코스를 그대로 들고 있고, 붙여 보내려면
  혜택·날씨를 다시 조립해야 해서 기상청 호출이 딸려온다
- 구성 검증은 저장과 같은 길을 탄다. 두 경로가 갈리면 같은 payload 가 한쪽에서만 통과한다 —
  CourseSaveRequest 의 출발지 검사·예외 번역을 한 곳으로 모아 두 팩토리가 함께 쓴다
- 발급한 링크가 실제로 열리고 공개 응답에 내부 courseId 가 없는지 확인한다
- **담지 않은 코스가 내 코스 목록에 안 나오는지**를 단언한다. 이 PR 의 전제가 "주인이 없으면
  게스트 범위 질의에 안 걸린다" 라, 그 전제가 깨지면 여기서 걸려야 한다
- 구성이 틀린 요청이 저장과 같은 400(ITINERARY-002)을 받는지 본다 — 두 경로의 검증이
  갈리는 회귀를 잡는다
- 같은 payload 를 두 번 보내면 링크가 두 개라는 것도 잠근다. 문서에 적은 동작이라 우연히
  바뀌면 안 된다
- 도메인 단위 테스트로 sharedOnly 가 주인 없이 만들어지고 구성 불변식은 그대로 지키는지 본다
@sevineleven sevineleven added the feat 새 기능 (외부에 보이는 변화) label Aug 13, 2026
@sevineleven sevineleven linked an issue Aug 13, 2026 that may be closed by this pull request
@sevineleven sevineleven self-assigned this Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sevineleven, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

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

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 04c37077-1b47-46a3-956a-db0555e12277

📥 Commits

Reviewing files that changed from the base of the PR and between c914aa2 and 1b5828e.

📒 Files selected for processing (9)
  • src/main/java/com/offway/core/itinerary/controller/CourseStorageApi.java
  • src/main/java/com/offway/core/itinerary/controller/CourseStorageController.java
  • src/main/java/com/offway/core/itinerary/controller/dto/CourseSaveRequest.java
  • src/main/java/com/offway/core/itinerary/controller/dto/CourseShareResponse.java
  • src/main/java/com/offway/core/itinerary/domain/Course.java
  • src/main/java/com/offway/core/itinerary/service/CoursePersistenceService.java
  • src/main/java/com/offway/core/itinerary/service/CourseStorageService.java
  • src/test/java/com/offway/core/itinerary/controller/CourseShareIntegrationTest.java
  • src/test/java/com/offway/core/itinerary/domain/CourseTest.java

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.

- 나눠 저장하면 코스만 커밋되고 링크 발급이 실패했을 때 아무도 닿을 수 없는 행이 남는다.
  주인이 없어 목록·상세·삭제 어디에도 안 걸리고, 공유 행이 없어 링크로도 못 연다
- 정리 배치가 공유 행의 발급 시각으로 나이를 재므로(코스 테이블에 생성 시각이 없다)
  그 배치조차 이 행을 못 찾는다 — 영영 남는 죽은 데이터가 된다
- "담지 않은 코스는 반드시 공유 행과 짝" 이라는 정리의 전제를 여기서 지킨다
- 이 경로는 발급 경합을 다루지 않는다. 방금 만든 코스라 그 id 를 아는 요청이 하나뿐이라
  유니크 제약에 걸릴 상대가 없다
- 정리 배치가 이 전제 위에 서는데 그것을 확인하는 단언이 없었다
@sevineleven

Copy link
Copy Markdown
Contributor Author

리뷰 (CodeRabbit 레이트리밋 대신 수동) + 대응

로컬 검증 — 테스트 1,315건 통과 · 실패 0 · 컨벤션 훅 차단 0.

좋았던 것

  • CourseSaveRequest.build(Function) 추출. 담기와 담지 않는 공유가 같은 검증을 타게 되어, 같은 payload 가 한쪽에서만 통과하는 일이 구조적으로 막힙니다.
  • 주인 없이 저장하는 선택. 플래그 컬럼이었다면 게스트 범위 질의 넷을 다 고쳐야 하고 하나만 빠뜨려도 남의 목록에 뜹니다. 소유 관계를 아예 안 만드는 쪽이 규칙 하나로 끝납니다.
  • POST /courses/sharePUBLIC_PATH_PATTERN 밖이라 인증 게이트 안에 있는 것도 확인했습니다.
  • 테스트가 전제("주인이 없으면 게스트 범위 질의에 안 걸린다")를 직접 단언합니다.

고친 것 — 코스와 공유 행이 짝이 아닐 수 있었다 → 641d3cc · 105e607

본문이 정리(retention) 근거로 이렇게 적었습니다:

담지 않은 코스는 반드시 공유 행과 짝이라 빠지는 것이 없다

그 전제가 성립하지 않았습니다. persist 와 토큰 발급이 별개 트랜잭션이라, 코스만 커밋되고 발급이 실패하면 아무도 닿을 수 없는 행이 남습니다 — 주인이 없어 목록·상세·삭제 어디에도 안 걸리고, 공유 행이 없어 링크로도 못 엽니다.

더 나쁜 것은 정리 배치조차 못 찾는다는 점입니다. 나이를 공유 행의 발급 시각으로 재는데(코스 테이블에 생성 시각이 없어서) 그 행이 없으니까요. 영영 남는 죽은 데이터가 됩니다.

CoursePersistenceService.persistWithShare 로 한 트랜잭션에 묶었습니다. 이 경로는 발급 경합을 다루지 않아도 됩니다 — 방금 만든 코스라 그 id 를 아는 요청이 하나뿐이라 유니크 제약에 걸릴 상대가 없습니다(shareOf 와 다른 점입니다).

테스트로도 잠갔습니다 — 발급한 토큰으로 공유 행을 찾고, 그 행이 가리키는 코스가 실제로 있는지 봅니다.

남겨둔 것 (지적 아님, 확인용)

  • 멱등하지 않음 — 같은 payload 를 두 번 보내면 링크가 둘, 주인 없는 코스도 둘 생깁니다. 문서화·테스트가 돼 있어 계약으로는 명확한데, 화면에서 공유 버튼을 두 번 누르는 것을 프론트가 막아주는지는 확인이 필요합니다.
  • @ApiResponse(401) 이 새 엔드포인트에만 붙어 파일 안에서 불일치가 남습니다. 일괄 정리는 별도 작업이 맞아 보입니다.
  • 정리 배치 없음 — 증가 속도를 보고 붙이는 판단에 동의합니다. 지울 근거(주인 없음 + 발급 시각)가 본문에 남아 있고, 이제 그 근거가 실제로 성립합니다.

…-saving

# Conflicts:
#	src/test/java/com/offway/core/itinerary/controller/CourseShareIntegrationTest.java
@sevineleven
sevineleven merged commit c20ff40 into dev Aug 13, 2026
4 checks passed
@sevineleven
sevineleven deleted the feat/261-share-without-saving branch August 13, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새 기능 (외부에 보이는 변화)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

담지 않고 공유 링크만 발급하는 API

1 participant