Skip to content

fix: iOS 인스타그램 스토리 공유가 미지원 안내로 막힘 - #503

Merged
ychany merged 5 commits into
devfrom
fix/502-ios-instagram-story
Aug 12, 2026
Merged

fix: iOS 인스타그램 스토리 공유가 미지원 안내로 막힘#503
ychany merged 5 commits into
devfrom
fix/502-ios-instagram-story

Conversation

@ychany

@ychany ychany commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

작업 요약

  • iOS 인스타그램 스토리 공유가 "미지원" 안내로 막히던 문제를 수정합니다.
  • 영수증 공유 이미지에 상품 사진이 빠지던 문제를 수정합니다.
  • 영수증 공유 시트의 스토리 버튼이 뒤늦게 나타나던 문제를 수정합니다.

작업 세부 내용

iOS 스토리 공유를 검증하다 영수증 공유 시트에서 두 가지 문제를 더 발견해 함께 고쳤습니다.

1. iOS 인스타그램 스토리 공유 (#502)

스토리 공유를 누르면 인스타그램이 열리지만 "이 앱은 스토리 공유를 지원하지 않습니다" 안내만 떴습니다. 안드로이드는 인텐트 경로라 영향이 없어 그동안 드러나지 않았습니다.

원인이 두 가지였습니다.

Facebook App ID 누락 — 2023-01 부터 인스타그램이 필수로 요구합니다. Info.plistFacebookAppID 가 없었고 딥링크에도 붙지 않았습니다. config plugin 으로 주입하고 딥링크에 ?source_application= 을 추가했습니다.

pasteboard 키 불일치 — 기존에는 expo-clipboard 로 일반 이미지 복사만 했습니다. 인스타그램은 전용 키를 읽습니다.

UIPasteboard.general.setItems(
  [["com.instagram.sharedSticker.backgroundImage": imageData]],
  options: [.expirationDate: ...]   // 5분 후 만료
)

expo-clipboard 로는 이 키를 지정할 수 없어 로컬 Expo 모듈(modules/instagram-story)을 추가했습니다.

App ID 는 app.json 플러그인 설정에만 두고, 런타임은 expo-constants 로 같은 값을 읽어 중복을 없앴습니다. 비밀값이 아니라 카카오 네이티브 키와 같은 성격입니다.

iOS 실기기에서 스토리 편집 화면 진입까지 확인했습니다.

2. 영수증 공유 이미지에 상품 사진 누락

html-to-image 가 캡처 시점에 이미지를 다시 fetch 하는데, mode: 'cors' 로 요청하고 있었습니다. /_next/image 프록시 응답에는 CORS 헤더가 없어 브라우저가 거부했고, 해당 이미지가 빈 칸으로 남았습니다.

$ curl -I "https://dev.piki.day/_next/image?url=...&w=128&q=75"
HTTP/2 200
vary: Accept
     ← Access-Control-Allow-Origin 없음

cache: 'force-cache' 덕에 브라우저 캐시가 맞아떨어질 때만 보여서 산발적으로 재현됐습니다.

동일 origin 이라 CORS 가 불필요하고, <img>crossOrigin 없이 로드되므로 mode 를 빼면 같은 캐시 항목을 재사용합니다.

- fetchRequestInit: { cache: 'force-cache', mode: 'cors' },
+ fetchRequestInit: { cache: 'force-cache' },

3. 스토리 버튼이 뒤늦게 나타남

앱 여부를 useSyncExternalStore 로만 판정해 SSR 스냅샷이 항상 false 였습니다. hydration 이 끝나야 true 가 되어, 시트가 이미 올라온 뒤 버튼이 하나 추가되며 줄 전체가 다시 배치됐습니다.

서버에서 User-Agent 로 판정해 내려주도록 바꿨습니다 (getIsApp, getIsGuest 와 같은 패턴). UA 를 안 붙이는 구버전 앱을 위해 클라 판정은 fallback 으로 남겼습니다.

const isAppEnvironment = isApp || isWebviewByClient;

참고

  • 안드로이드 스토리 공유 경로는 수정하지 않았습니다.
  • expo-modules-core 를 명시적 의존성으로 추가했습니다 — pnpm 스토어에만 있어 로컬 모듈에서 import 가 안 됐습니다.
  • expo-clipboard 는 이제 사용처가 없습니다. 제거는 별도로 판단하는 게 좋겠습니다.

스크린샷

IMG_3166

연관 이슈

closes #502

Summary by CodeRabbit

  • 새 기능

    • iOS에서 이미지를 Instagram Stories 배경으로 직접 공유할 수 있습니다.
    • 결과 화면이 앱 환경을 자동으로 인식해 공유 기능을 적절히 제공합니다.
  • 개선

    • 앱 여부를 서버에서 판단해 웹뷰 환경에서도 공유 동작의 일관성을 높였습니다.
    • 결과 이미지 공유 시 이미지 로딩 및 캐시 처리를 개선했습니다.
  • 버그 수정

    • Instagram 미설치, 설정 누락 또는 공유 실패 상황을 구분해 처리합니다.

ychany added 3 commits August 12, 2026 21:38
일반 이미지 복사 + App ID 없는 딥링크로는 인스타그램이 요청을 거부한다.
안드로이드는 인텐트 경로라 영향이 없어 그동안 드러나지 않았다.

- 전용 pasteboard 키(com.instagram.sharedSticker.backgroundImage)로
  이미지를 넘기는 네이티브 모듈 추가 (expo-clipboard 로는 지정 불가)
- Facebook App ID 를 Info.plist 와 딥링크에 주입 (2023-01 부터 필수)
- App ID 는 app.json 플러그인 설정에 두고 런타임이 같은 값을 읽어 중복 제거
html-to-image 가 이미지를 다시 fetch 할 때 mode:'cors' 로 요청하는데
/_next/image 프록시 응답에는 CORS 헤더가 없어 브라우저가 거부했다.
브라우저 캐시에 우연히 맞는 항목이 있을 때만 보여 산발적으로 재현됐다.

동일 origin 이라 CORS 가 불필요하고, img 태그도 crossOrigin 없이
로드되므로 mode 를 빼면 같은 캐시 항목을 재사용한다.
앱 여부를 useSyncExternalStore 로만 판정해 SSR 스냅샷이 항상 false 였다.
hydration 이 끝나야 true 가 되어, 시트가 열린 뒤 스토리 공유 버튼이
갑자기 추가되며 버튼 줄이 다시 배치됐다.

서버에서 User-Agent 로 판정해 내려주고, UA 가 없는 구버전 앱을 위해
클라 판정을 fallback 으로 남긴다.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
piki Ready Ready Preview Aug 12, 2026 5:16pm

@github-actions github-actions Bot added APP Good for newcomers fix Something isn't working WEB labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 48 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c3c1ec3-ef19-4dbc-bd8a-3fdc75c5b514

📥 Commits

Reviewing files that changed from the base of the PR and between 2e81a57 and 41f5079.

📒 Files selected for processing (4)
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareCaptureLayer.tsx
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts
  • apps/web/src/app/tournament/[id]/result/page.tsx
📝 Walkthrough

Walkthrough

iOS Instagram Story 공유를 네이티브 Expo 모듈 기반으로 변경했습니다. Facebook App ID와 전용 pasteboard를 사용합니다. 결과 화면은 서버에서 판정한 앱 여부를 공유 컴포넌트에 전달합니다.

Changes

Instagram Story 공유

Layer / File(s) Summary
네이티브 모듈과 Expo 등록
apps/app/modules/instagram-story/..., apps/app/package.json
iOS InstagramStoryModule과 CocoaPods 설정을 추가했습니다. Base64 이미지 디코딩, Instagram 설치 확인, 전용 pasteboard 저장, Stories URL 호출을 구현했습니다.
앱 설정과 공유 호출
apps/app/app.json, apps/app/plugins/withInstagramStoryShare.js, apps/app/utils/handleInstagramStory.ts
Expo 설정에 Facebook App ID를 연결하고 Info.plist에 기록합니다. 기존 clipboard 및 딥링크 호출을 네이티브 공유 함수 호출로 변경했습니다.
결과 화면 앱 판정 전달
apps/web/src/utils/getIsApp.ts, apps/web/src/app/tournament/[id]/result/...
서버의 user-agent로 앱 여부를 판정합니다. 결과 화면과 공유 다이얼로그에 isApp을 전달합니다. 이미지 요청에서는 force-cache를 유지합니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ResultPage
  participant ResultClient
  participant ReceiptShareDialog
  participant handleInstagramStory
  participant InstagramStoryModule
  ResultPage->>ResultPage: user-agent로 isApp 판정
  ResultPage->>ResultClient: isApp 전달
  ResultClient->>ReceiptShareDialog: isApp 전달
  ReceiptShareDialog->>handleInstagramStory: Instagram Story 공유 요청
  handleInstagramStory->>InstagramStoryModule: base64 이미지와 Facebook App ID 전달
  InstagramStoryModule->>InstagramStoryModule: 전용 pasteboard 저장 및 Stories URL 생성
  InstagramStoryModule-->>handleInstagramStory: 공유 상태 반환
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 iOS 인스타그램 스토리 공유 차단 문제라는 변경의 핵심을 정확히 설명합니다.
Linked Issues check ✅ Passed Facebook App ID, source_application, 전용 pasteboard 키를 구현했으며 Android 공유 경로는 변경하지 않아 이슈 #502의 코딩 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 영수증 이미지 누락과 스토리 버튼 표시 지연 수정은 PR 목표에 포함되며, 나머지 변경도 iOS 공유 기능 구현에 필요합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/502-ios-instagram-story

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/app/modules/instagram-story/src/index.ts`:
- Around line 23-26: Update the multiline parameter lists for
shareBackgroundImage and the similarly structured declaration around lines 38–41
by adding trailing commas after their final parameters, including facebookAppId,
to comply with the TypeScript style rule.

In `@apps/app/plugins/withInstagramStoryShare.js`:
- Line 1: Apply a file-level ESLint override to the CommonJS entry point using
Node globals and disabling `@typescript-eslint/no-require-imports`, while
preserving the existing withInfoPlist import and module.exports behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 061d4faa-6536-4ec1-9408-1160ff7b652d

📥 Commits

Reviewing files that changed from the base of the PR and between f53b255 and 2e81a57.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/app/app.json
  • apps/app/modules/instagram-story/expo-module.config.json
  • apps/app/modules/instagram-story/ios/InstagramStory.podspec
  • apps/app/modules/instagram-story/ios/InstagramStoryModule.swift
  • apps/app/modules/instagram-story/package.json
  • apps/app/modules/instagram-story/src/index.ts
  • apps/app/package.json
  • apps/app/plugins/withInstagramStoryShare.js
  • apps/app/utils/handleInstagramStory.ts
  • apps/web/src/app/tournament/[id]/result/_components/ResultClient.tsx
  • apps/web/src/app/tournament/[id]/result/_components/receipt-share-dialog/ReceiptShareDialog.tsx
  • apps/web/src/app/tournament/[id]/result/_utils/shareReceiptImage.ts
  • apps/web/src/app/tournament/[id]/result/page.tsx
  • apps/web/src/utils/getIsApp.ts

Comment on lines +23 to +26
shareBackgroundImage: (
base64: string,
facebookAppId: string
) => Promise<ShareInstagramStoryStatusT>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

멀티라인 매개변수에 후행 쉼표를 추가하세요.

Line 25와 Line 40의 마지막 매개변수 뒤에 ES5 후행 쉼표가 없습니다. TypeScript 스타일 규칙에 맞게 추가하세요.

수정 예시
   shareBackgroundImage: (
     base64: string,
-    facebookAppId: string
+    facebookAppId: string,
   ) => Promise<ShareInstagramStoryStatusT>;
@@
 export const shareInstagramStoryBackground = async (
   base64: string,
-  facebookAppId: string
+  facebookAppId: string,
 ): Promise<ShareInstagramStoryStatusT> => {

Also applies to: 38-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/modules/instagram-story/src/index.ts` around lines 23 - 26, Update
the multiline parameter lists for shareBackgroundImage and the similarly
structured declaration around lines 38–41 by adding trailing commas after their
final parameters, including facebookAppId, to comply with the TypeScript style
rule.

Source: Coding guidelines

@@ -0,0 +1,26 @@
const { withInfoPlist } = require('@expo/config-plugins');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

pnpm install --frozen-lockfile
pnpm exec eslint apps/app/plugins/withInstagramStoryShare.js

Repository: TeamPiKi/client

Length of output: 2860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- plugin ---'
cat -n apps/app/plugins/withInstagramStoryShare.js

printf '%s\n' '--- nearby config plugins ---'
find apps/app/plugins -maxdepth 1 -type f -print | sort | while read -r file; do
  printf '\n### %s\n' "$file"
  sed -n '1,12p' "$file"
done

printf '%s\n' '--- ESLint configuration references ---'
rg -n --hidden --glob '!node_modules/**' \
  'no-require-imports|no-undef|eslint|withInfoPlist|module\.exports|export default' \
  .eslintrc* eslint.config.* package.json apps/app/package.json apps/app/plugins 2>/dev/null || true

printf '%s\n' '--- package scripts ---'
node -e "const p=require('./package.json'); console.log(JSON.stringify(p.scripts,null,2))"

Repository: TeamPiKi/client

Length of output: 5707


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ESLint configuration ---'
cat -n eslint.config.mjs | sed -n '1,90p'

printf '%s\n' '--- app scripts and lint configuration ---'
node - <<'JS'
const root = require('./package.json');
const app = require('./apps/app/package.json');
console.log('root scripts:', JSON.stringify(root.scripts, null, 2));
console.log('app scripts:', JSON.stringify(app.scripts, null, 2));
JS

printf '%s\n' '--- effective rules for the plugin ---'
pnpm exec eslint --print-config apps/app/plugins/withInstagramStoryShare.js |
  node -e "
    let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
      const c=JSON.parse(s);
      for (const k of ['no-undef','`@typescript-eslint/no-require-imports`']) {
        console.log(k + ':', JSON.stringify(c.rules[k]));
      }
    });
  "

printf '%s\n' '--- all CommonJS plugin entry points ---'
rg -n -U '^const .*require\\(|^module\\.exports' apps/app/plugins

Repository: TeamPiKi/client

Length of output: 3825


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lint results for all Config Plugin files ---'
pnpm exec eslint apps/app/plugins --format stylish || true

printf '%s\n' '--- CommonJS entry points ---'
rg -n '^(const .*require\\(|module\\.exports)' apps/app/plugins

printf '%s\n' '--- Node globals available to ESLint ---'
node - <<'JS'
const globals = require('globals');
console.log(JSON.stringify({
  require: globals.node.require,
  module: globals.node.module,
}, null, 2));
JS

printf '%s\n' '--- Expo lint command resolution ---'
pnpm --filter piki-app exec expo lint --help 2>&1 | sed -n '1,80p' || true

Repository: TeamPiKi/client

Length of output: 2712


Config Plugin 파일에 CommonJS ESLint 예외를 적용하세요.

apps/app/plugins의 CommonJS 엔트리 포인트에서 require()module.exports가 lint 오류를 발생시킵니다. 해당 디렉터리에 Node 전역을 적용하고 @typescript-eslint/no-require-imports를 비활성화하세요.

🧰 Tools
🪛 ESLint

[error] 1-1: A require() style import is forbidden.

(@typescript-eslint/no-require-imports)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/app/plugins/withInstagramStoryShare.js` at line 1, Apply a file-level
ESLint override to the CommonJS entry point using Node globals and disabling
`@typescript-eslint/no-require-imports`, while preserving the existing
withInfoPlist import and module.exports behavior.

Source: Linters/SAST tools

종이 폭은 그대로인데 내용이 시안보다 작았다. 시안 실측 기준
썸네일이 종이 폭의 23.1% 여야 하는데 16.2% 였다.

- zoom 1.28 → 1.42 (렌더 폭이 줄어 종이 폭 370px 은 유지)
- fitZoom 축소 시 종이 폭까지 좁아지던 문제도 함께 보정
@ychany
ychany merged commit 199a640 into dev Aug 12, 2026
6 of 7 checks passed
@ychany
ychany deleted the fix/502-ios-instagram-story branch August 12, 2026 17:14
iOdiO89 added a commit that referenced this pull request Aug 13, 2026
게이트가 1.1.2 로 잡혀 있었지만 앱쪽 핸들러(handleInstagramStory.ts)는
app-v1.2.0 에 처음 들어갔다. 1.1.x 유저는 게이트를 통과해 메시지를 보내지만
앱에 수신 case 가 없어 응답이 오지 않고, 15 초 타임아웃 뒤 실패 토스트만 떴다.

iOS 는 1.2.0 에서도 동작하지 않는다. 인스타그램 전용 pasteboard 키를 쓰는
네이티브 모듈(#503)이 app-v1.2.0 태그 이후에 추가돼 아직 어떤 릴리즈에도
포함되지 않았다. 따라서 실제로 동작하는 최초 버전은 1.2.1 이다.

- BRIDGE_GATE 의 minAppVersion 을 1.2.1 로 정정
- app.json version 을 1.2.1 로 올려 게이트 기준과 다음 빌드를 일치시킴
  (buildNumber/versionCode 는 eas.json 의 remote + autoIncrement 가 처리)

이제 1.2.1 미만에서는 전송 전에 차단돼 "앱을 업데이트하면 사용할 수 있어요"
안내가 뜬다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP Good for newcomers fix Something isn't working WEB

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: iOS 인스타그램 스토리 공유가 미지원 안내로 막힘

1 participant