사용자가 캐릭터 카드를 공유할 때 두 가지 방식을 제공한다.
| 방식 | 설명 |
|---|---|
| 링크 공유 | 캐릭터 카드 URL을 공유. SNS/메신저에서 OG 태그 기반 미리보기 카드 표시 |
| 이미지 다운로드 | 캐릭터 카드를 이미지 파일로 변환하여 로컬 저장 |
링크 공유 시 카카오톡, 디스코드 등 메신저에서 미리보기 카드가 표시되도록 OG 태그를 설정한다. SNS/메신저가 URL을 감지하면 해당 서버가 직접 URL에 접근하여 HTML <head>의 OG 태그를 읽어 카드를 생성한다.
| 태그 | 역할 |
|---|---|
og:title | 카드에 표시되는 제목 |
og:description | 카드에 표시되는 설명 |
og:image | 썸네일 이미지 (R2에 저장된 캐릭터 이미지 URL) |
og:url | 실제 페이지 URL |
og:image는 반드시 절대 경로(https://...)로 지정해야 한다. 상대 경로로 지정 시 외부 서버에서 이미지를 찾지 못한다.이미지 생성 방식은 서버 생성과 프론트엔드 생성(html2canvas) 두 가지가 있다.
| 항목 | 서버 생성 | html2canvas (채택) |
|---|---|---|
| 구현 위치 | Java (서버) | 브라우저 (JS) |
| 디자인 반영 | 코드로 직접 조립 | 화면에 보이는 그대로 캡처 |
| 서버 부하 | 있음 | 없음 |
| 캐릭터+아이템 합성 | 별도 구현 필요 | CSS 레이아웃 그대로 캡처 |
캐릭터 위에 아이템을 겹쳐 합성하는 구조이므로, 화면에 렌더링된 결과를 그대로 이미지로 변환하는 html2canvas 방식이 적합하다. 서버 부하도 없고 디자인 변경 시 별도 수정이 필요 없다.
html2canvas 또는 dom-to-image (프론트엔드 JS 라이브러리)
공유 보상(별조각)은 하루 최대 1회로 제한하며, 중복 지급을 막기 위해 비즈니스 로직과 DB 제약조건 두 단계로 검증한다.
사용자가 공유 이벤트를 발생시키면 ShareService에서 오늘 날짜(LocalDate today)를 기준으로 DB를 먼저 조회한다.
// 오늘 이미 보상을 받은 기록이 있는지 확인
boolean alreadyRewarded = shareLogRepository
.existsByUserIdAndShareDateAndRewardPaidTrue(userId, today);
// 이미 받았으면 0, 처음이면 50 지급
boolean rewardPaid = !alreadyRewarded;
int rewardAmount = rewardPaid ? 50 : 0;
보상 없이 기록만 남기는 경우 rewardPaid = false로 저장한다.
비즈니스 로직만으로는 공유 버튼을 짧은 순간에 두 번 클릭하는 경우(Race Condition) 두 요청이 동시에 통과할 수 있다. 이를 방지하기 위해 프론트엔드가 요청 시 고유한 멱등성 키를 생성하여 함께 전송하도록 설계한다.
SHARE_REWARD:{userId}:{yyyy-MM-dd}share_logs 테이블에 해당 키에 UNIQUE 제약조건을 걸어두어, 동시에 두 요청이 로직을 통과하더라도 DB 저장 시점에 하나는 중복 키 에러로 차단된다.
CONSTRAINT uq_share_logs_idempotency_key UNIQUE (idempotency_key)
캐릭터 카드 공유에 사용할 이미지 스토리지로 Cloudflare R2를 채택한다.
운영자가 캐릭터 이미지를 미리 업로드해두고, 사용자가 카드를 공유할 때 해당 이미지 URL을 내려주는 구조다. 이 구조에서 핵심 비용은 이미지를 사용자에게 전달할 때 발생하는 egress(데이터 전송) 비용이다.
| 항목 | Cloudflare R2 | AWS S3 |
|---|---|---|
| Egress 비용 | 무료 | GB당 $0.09 |
| 스토리지 비용 | GB당 $0.015 | GB당 $0.023 |
| 무료 티어 | 스토리지 10GB, 읽기 1000만 건/월 | 12개월 한정 |
| S3 호환 API | 지원 | - |
| 이미지 변환 | 미지원 | 미지원 |
트래픽이 크게 증가하거나 AWS 생태계와의 깊은 통합이 필요해지는 시점에 S3로 전환을 검토한다.