Conversation
[fix] 레이아웃이 깨지는 버그 수정
…-636 지원하기 버튼 테스트 수정
- Android: intent URL 스킴으로 앱 실행 (미설치 시 Play Store 이동) - iOS: 카카오톡 외부 브라우저로 열어 Universal Link 트리거
- 앱 웹뷰 / 카카오톡 / 일반 브라우저 3단계 조건 분기 적용 - 카카오톡 환경에서만 앱열기 버튼 노출
- intent URL 스킴 대신 kakaotalk://web/openExternal로 외부 브라우저 열기 - Android App Links(assetlinks.json) 기반으로 바로 앱 실행되도록 개선 - iOS/Android 동일한 로직으로 단순화
- intent URL에 S.browser_fallback_url 파라미터 추가 - 앱 설치 시 바로 실행, 미설치 시 Play Store로 자동 리다이렉트 - iOS는 Safari 외부 브라우저 방식 유지
- iOS에서도 프로덕션 도메인(www.moadong.com) URL을 사용하도록 buildProductionUrl 추가 - kakaotalk://web/openExternal 실패 시 1.5초 후 App Store로 자동 이동 - ANDROID_HOST를 APP_HOST로 통합하여 양 플랫폼 공통 사용
- Safari에서 프로덕션 URL 대신 App Store 링크를 직접 열도록 수정 - 설치됨 → App Store에서 "열기", 미설치 → "받기"로 양쪽 케이스 대응 - 동작 불가능했던 document.hidden 기반 타임아웃 폴백 로직 제거
- Safari에서 프로덕션 URL 열기로 앱 설치 시 바로 실행되도록 변경 - 미설치 시 Smart App Banner를 통해 App Store 유도
- moadongapp:// 커스텀 스킴으로 앱 직접 실행 시도 - 2초 후에도 페이지가 visible이면 앱 미설치로 판단 → App Store 이동 - 앱이 열려 페이지가 hidden 되면 타이머 취소
This reverts commit 12a427e.
- 상대경로 제외
- useRef로 타이머를 추적해 중복 호출 시 이전 타이머 취소 - useEffect 정리 함수로 컴포넌트 언마운트 시 잔여 타이머 정리
Co-authored-by: Cursor <cursoragent@cursor.com>
…ink-fix-MOA-665 [Feature] 카카오톡 링크 내 유니버셜 링크/앱링크 미동작 대응
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
카카오톡 앱 열기 기능 frontend/src/hooks/useOpenAppFromKakao.ts, frontend/src/utils/isKakaoTalkBrowser.ts |
새 훅과 유틸리티를 추가하여 카카오톡 브라우저 감지 및 딥링크/스토어 폴백을 통한 앱 열기 기능 구현 |
ClubDetailTopBar 통합 frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx, frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts |
카카오톡 환경에서 앱 열기 버튼과 로딩 오버레이를 렌더링하는 UI 추가 및 스타일 컴포넌트 정의 |
테스트 및 구성 업데이트 frontend/src/utils/getDeadLineText.test.ts, frontend/src/App.tsx |
마감 텍스트 테스트 케이스 확장 및 GlobalStyles/GlobalBoundary 래퍼 순서 조정 |
Sequence Diagram(s)
sequenceDiagram
actor User as 사용자
participant ClubDetailTopBar
participant useOpenAppFromKakao
participant isKakaoTalkBrowser
participant KakaoIntent as Kakao Deep Link
participant AppStore as App Store
User->>ClubDetailTopBar: 앱 열기 버튼 클릭
ClubDetailTopBar->>isKakaoTalkBrowser: 카카오톡 브라우저 확인
isKakaoTalkBrowser-->>ClubDetailTopBar: true/false
rect rgba(100, 150, 200, 0.5)
ClubDetailTopBar->>useOpenAppFromKakao: openApp() 호출
useOpenAppFromKakao->>useOpenAppFromKakao: 플랫폼 감지 (Android/iOS)
useOpenAppFromKakao->>useOpenAppFromKakao: 딥링크 URL 구성
useOpenAppFromKakao->>KakaoIntent: 카카오 intent 또는 custom scheme 전달
end
alt 앱이 2초 내에 열림
KakaoIntent-->>useOpenAppFromKakao: 앱 열기 성공
useOpenAppFromKakao->>useOpenAppFromKakao: 타이머 정리, isLoading = false
else 앱이 설치되지 않음
useOpenAppFromKakao->>AppStore: 2초 후 App Store로 리다이렉트
AppStore-->>User: 스토어 페이지 표시
end
useOpenAppFromKakao-->>ClubDetailTopBar: { openApp, isLoading }
ClubDetailTopBar-->>User: 로딩 상태 표시 업데이트
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
- [release] FE v1.1.22 #1172: 마감 텍스트 포맷을 "지난 모집・YYYY M/D"로 변경하는 getDeadLineText 로직과 테스트 케이스 업데이트 관련
- [Feature] 카카오톡 링크 내 유니버셜 링크/앱링크 미동작 대응 #1218: 동일한 카카오톡 앱 열기 기능(useOpenAppFromKakao, isKakaoTalkBrowser, AppOpenButton)을 다루는 PR
- [fix] 모바일 앱에서 앱스토어 배너/팝업 링크 이동 안되는 문제 수정 #1041: useOpenAppFromKakao 훅이 의존하는 APP_STORE_LINKS 수정 및 getAppStoreLink 동작 변경 관련
Suggested labels
💻 FE, 📈 release
Suggested reviewers
- lepitaaar
- suhyun113
- oesnuj
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Title check | ❓ Inconclusive | Pull request title '[release] FE v1.1.25'는 버전 릴리스를 나타내지만, 구체적인 변경 내용을 명확히 전달하지 못합니다. | 제목을 더 구체적으로 수정하여 주요 변경 사항을 반영하세요. 예: '[release] FE v1.1.25: Add KakaoTalk app opening support' 또는 유사한 형태로 작성하시기 바랍니다. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
- 📝 Generate docstrings (stacked PR)
- 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
develop-fe
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts (1)
4-16: 기존TopBarWrapper의 하드코딩된z-index값 정리 (선택)
LoadingOverlay는Z_INDEX.overlay상수를 사용하지만,TopBarWrapper는z-index: 100으로 하드코딩되어 있습니다.Z_INDEX상수(Z_INDEX.header = 1000)로 통일하면 프로젝트 전체에서 스태킹 컨텍스트를 한 곳에서 관리할 수 있습니다.♻️ 선택적 개선 제안
export const TopBarWrapper = styled.div<{ $isVisible: boolean }>` position: fixed; top: 0; left: 0; right: 0; - z-index: 100; + z-index: ${Z_INDEX.header};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts` around lines 4 - 16, Replace the hardcoded z-index in TopBarWrapper with the shared Z_INDEX constant to centralize stacking order management; update the styled component TopBarWrapper to use Z_INDEX.header (the same constant used by LoadingOverlay) instead of 100 so the header stacking context is controlled from the Z_INDEX enum/object.frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx (1)
133-135:onClick래퍼 화살표 함수 제거 (선택)
onClick={() => openApp()}은 매 렌더링마다 새로운 함수를 생성합니다. 직접 참조를 전달하면 충분합니다.♻️ 선택적 개선 제안
- <Styled.AppOpenButton onClick={() => openApp()}> + <Styled.AppOpenButton onClick={openApp}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx` around lines 133 - 135, The button's onClick currently creates a new lambda each render via onClick={() => openApp()}; change it to pass the handler directly—use onClick={openApp} on Styled.AppOpenButton (or bind appropriately if openApp requires arguments or a specific this) to avoid unnecessary function recreation and improve render performance.frontend/src/utils/getDeadLineText.test.ts (1)
97-107:describe블록 외부에 위치한 테스트 케이스Lines 97–107의 두
it()블록이describe('getDeadlineText 함수 테스트', () => {...})외부에 선언되어 있습니다. 기능적으로는 동작하지만, 테스트 리포트에서 그룹화가 깨지고 파일 내 일관성이 떨어집니다.♻️ 제안: describe 블록 내부로 이동
-}); - -it('CLOSED 상태이고 종료일이 있으면 지난 모집 날짜를 표시한다', () => { - expect(getDeadlineText(null, new Date('2024-12-31'), 'CLOSED')).toBe( - '지난 모집・2024 12/31', - ); -}); - -it('UPCOMING 상태이고 시작일이 있으면 모집 시작일을 표시한다', () => { - expect( - getDeadlineText(new Date('2024-11-15T14:30:00'), null, 'UPCOMING'), - ).toBe('11월 15일 14:30 모집 시작'); -}); + + it('CLOSED 상태이고 종료일이 있으면 지난 모집 날짜를 표시한다', () => { + expect(getDeadlineText(null, new Date('2024-12-31'), 'CLOSED')).toBe( + '지난 모집・2024 12/31', + ); + }); + + it('UPCOMING 상태이고 시작일이 있으면 모집 시작일을 표시한다', () => { + expect( + getDeadlineText(new Date('2024-11-15T14:30:00'), null, 'UPCOMING'), + ).toBe('11월 15일 14:30 모집 시작'); + }); +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/utils/getDeadLineText.test.ts` around lines 97 - 107, The two standalone it() tests for getDeadlineText should be moved inside the existing describe('getDeadlineText 함수 테스트', () => { ... }) block so they are grouped with the other specs; locate the two it(...) blocks that assert CLOSED and UPCOMING behaviors and cut-and-paste them into the describe block (keeping their contents unchanged) so test reports are consistently grouped under getDeadlineText and file structure remains consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/hooks/useOpenAppFromKakao.ts`:
- Around line 12-16: The visibilitychange listener added inside openApp is never
removed, causing leaked/accumulating listeners; fix by tracking the listener
callback in a ref (e.g., visibilityHandlerRef) and always call
document.removeEventListener('visibilitychange', visibilityHandlerRef.current)
when clearing timer or in the useEffect cleanup, and also remove any existing
listener before adding a new one in openApp; ensure timerRef and the visibility
handler ref are both cleared/nullified after removal.
---
Nitpick comments:
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.styles.ts`:
- Around line 4-16: Replace the hardcoded z-index in TopBarWrapper with the
shared Z_INDEX constant to centralize stacking order management; update the
styled component TopBarWrapper to use Z_INDEX.header (the same constant used by
LoadingOverlay) instead of 100 so the header stacking context is controlled from
the Z_INDEX enum/object.
In
`@frontend/src/pages/ClubDetailPage/components/ClubDetailTopBar/ClubDetailTopBar.tsx`:
- Around line 133-135: The button's onClick currently creates a new lambda each
render via onClick={() => openApp()}; change it to pass the handler directly—use
onClick={openApp} on Styled.AppOpenButton (or bind appropriately if openApp
requires arguments or a specific this) to avoid unnecessary function recreation
and improve render performance.
In `@frontend/src/utils/getDeadLineText.test.ts`:
- Around line 97-107: The two standalone it() tests for getDeadlineText should
be moved inside the existing describe('getDeadlineText 함수 테스트', () => { ... })
block so they are grouped with the other specs; locate the two it(...) blocks
that assert CLOSED and UPCOMING behaviors and cut-and-paste them into the
describe block (keeping their contents unchanged) so test reports are
consistently grouped under getDeadlineText and file structure remains
consistent.
| useEffect(() => { | ||
| return () => { | ||
| if (timerRef.current !== null) clearTimeout(timerRef.current); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
visibilitychange 리스너가 컴포넌트 언마운트 시 제거되지 않아 이벤트 리스너가 누적됩니다
openApp()이 호출될 때마다 document에 visibilitychange 리스너가 추가되지만, useEffect 정리 함수는 타이머만 해제하고 리스너는 제거하지 않습니다.
문제점:
- 언마운트 시 누수:
visibilitychange가 발생하기 전에 컴포넌트가 언마운트되면, 리스너가document에 잔류하여 이후 다른 가시성 변경 이벤트에서 오래된 콜백이 실행됩니다. - 리스너 누적: iOS에서
openApp()을 여러 번 호출하면{ once: true }리스너가 계속 쌓입니다.visibilitychange가 발생하기 전까지는 중복 리스너가document에 존재합니다.
🐛 제안 수정
const useOpenAppFromKakao = () => {
const [isLoading, setIsLoading] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ const visibilityHandlerRef = useRef<(() => void) | null>(null);
useEffect(() => {
return () => {
if (timerRef.current !== null) clearTimeout(timerRef.current);
+ if (visibilityHandlerRef.current !== null) {
+ document.removeEventListener('visibilitychange', visibilityHandlerRef.current);
+ visibilityHandlerRef.current = null;
+ }
};
}, []);
const openApp = () => {
// ... Android 처리 ...
+ // 이전 리스너 제거 후 새 리스너 등록
+ if (visibilityHandlerRef.current !== null) {
+ document.removeEventListener('visibilitychange', visibilityHandlerRef.current);
+ visibilityHandlerRef.current = null;
+ }
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
setIsLoading(true);
const url = new URL(currentUrl);
window.location.href = `${IOS_SCHEME}://${url.pathname}${url.search}${url.hash}`;
timerRef.current = setTimeout(() => {
timerRef.current = null;
+ visibilityHandlerRef.current = null;
setIsLoading(false);
if (!document.hidden) {
window.location.href = `kakaotalk://web/openExternal?url=${encodeURIComponent(APP_STORE_LINKS.iphone)}`;
}
}, 2000);
- document.addEventListener(
- 'visibilitychange',
- () => {
- if (document.hidden && timerRef.current !== null) {
- clearTimeout(timerRef.current);
- timerRef.current = null;
- setIsLoading(false);
- }
- },
- { once: true },
- );
+ const handleVisibilityChange = () => {
+ if (document.hidden && timerRef.current !== null) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ visibilityHandlerRef.current = null;
+ setIsLoading(false);
+ }
+ };
+ visibilityHandlerRef.current = handleVisibilityChange;
+ document.addEventListener('visibilitychange', handleVisibilityChange, { once: true });
};Also applies to: 48-58
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useOpenAppFromKakao.ts` around lines 12 - 16, The
visibilitychange listener added inside openApp is never removed, causing
leaked/accumulating listeners; fix by tracking the listener callback in a ref
(e.g., visibilityHandlerRef) and always call
document.removeEventListener('visibilitychange', visibilityHandlerRef.current)
when clearing timer or in the useEffect cleanup, and also remove any existing
listener before adding a new one in openApp; ensure timerRef and the visibility
handler ref are both cleared/nullified after removal.
#️⃣연관된 이슈
📝작업 내용
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
릴리스 노트
새 기능
테스트