Skip to content

feat: PWA 관련 UX 작업#361

Open
gajaeup wants to merge 12 commits into
devfrom
feat/SM-209
Open

feat: PWA 관련 UX 작업#361
gajaeup wants to merge 12 commits into
devfrom
feat/SM-209

Conversation

@gajaeup

@gajaeup gajaeup commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

❓ 이슈

✍️ Description

PWA L1 — 커스텀 설치 프롬프트 UI 구현

  • src/hooks/usePWAInstall.ts (신규)
    beforeinstallprompt 이벤트 캡처 → deferredPrompt state 저장
    iOS 감지: useSyncExternalStore + navigator.userAgent (SSR safe)
    display-mode: standalone 감지: 기존 useMediaQuery 재사용
    promptInstall(): 브라우저 설치 프롬프트 실행, accepted 시 버튼 자동 숨김

  • src/components/PWAInstallButton/index.tsx (신규)
    isInstallable || isIOS 가 false면 null 리턴 (설치 불가 환경, 이미 설치됨 포함)
    iOS: 클릭 시 IOSInstallGuideBottomSheet 오픈
    그 외: 클릭 시 promptInstall() 호출 → 브라우저 네이티브 프롬프트
    className prop으로 배치 시 스타일 커스텀 가능

  • src/components/PWAInstallButton/IOSInstallGuideBottomSheet.tsx (신규)
    iOS Safari는 beforeinstallprompt 미지원 → "공유 → 홈 화면에 추가" 3단계 안내
    기존 BottomSheet 컴파운드 컴포넌트 재사용

📸 스크린샷 (UI 변경 시)

✅ Checklist

PR

  • Branch Convention 확인

    feat/* 기능 구현, fix/* 버그 수정, refactor/* 개선, chore/* 설정/환경

  • Base Branch 확인
  • 적절한 Label 지정
  • Assignee 및 Reviewer 지정

Test

  • 로컬 작동 확인
  • 빌드 통과 (npm run build)
  • 린트 통과 (npm run lint)

Additional Notes

  • 추후 설치 버튼 배치 논의
  • 버튼 디자인, IOSInstallGuideBottomSheet.tsx 내용 확인

@gajaeup gajaeup self-assigned this Jun 20, 2026
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
completionisland Ready Ready Preview, Comment Jul 20, 2026 5:07am

@claude

claude Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gajaeup's task in 2m 45s —— View job


증분 리뷰 완료

이전 지적사항 해결 여부

해결됨 ✅

  • PWAInstallButton/index.tsx - className 우선순위 이슈 (cn() 사용으로 해결)
  • PWAInstallButton/index.tsx - Import 그룹 분리
  • PWAInstallBanner/index.tsx - Import 그룹 분리
  • usePushNotificationSetting.ts - Import 그룹 분리
  • NotificationSettingsSection/index.tsx - Import 그룹 분리

미해결 ❌

  • Header/index.tsx:8-15 - 상대 경로 import 순서 (인라인 코멘트 참조)

새로 발견된 이슈

없음

종합

PWA 설치 기능 구현 자체는 잘 작성되었습니다:

  • usePWAInstall 훅의 SSR-safe iOS 감지 (useSyncExternalStore 활용)
  • 브라우저별 분기 (beforeinstallprompt vs iOS 수동 안내)
  • 적절한 'use client' 경계 설정 (leaf 컴포넌트에만 사용)
  • 서버/클라이언트 컴포넌트 분리 원칙 준수

다만 이전 리뷰의 Header/index.tsx import 순서 이슈가 아직 해결되지 않았습니다. 내부 모듈(@/...) import를 먼저 배치하고, 상대 경로(./) import를 별도 그룹으로 분리해주세요.


Comment on lines +3 to +6
import { useState } from 'react';

import { Button } from '@/components/ui/Button';
import { usePWAInstall } from '@/hooks/usePWAInstall';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Import 그룹 사이 빈 줄 누락

코드 스타일 규칙에 따르면 import 그룹 사이에 빈 줄이 필요합니다:

  1. React / Next.js
  2. (빈 줄)
  3. 외부 라이브러리
  4. (빈 줄)
  5. 내부 모듈
  6. (빈 줄)
  7. 상대 경로
Suggested change
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { usePWAInstall } from '@/hooks/usePWAInstall';

@strawberryroll

Copy link
Copy Markdown
Collaborator

고생하셨어요! 버튼 배치나 디자인은 디자이너님과도 함께 논의해보는 것이 좋을 것 같아요


return (
<>
<Button className='text-body-02-m text-gray-700' onClick={handleClick} {...buttonProps}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] className 우선순위 이슈

현재 {...buttonProps} spread가 인라인 className 뒤에 위치하여, buttonPropsclassName이 포함되면 인라인 className이 무시됩니다.

해결 방법:

  1. cn()으로 병합하거나
  2. spread를 먼저 배치하고 인라인 className을 뒤에 위치
Suggested change
<Button className='text-body-02-m text-gray-700' onClick={handleClick} {...buttonProps}>
<Button onClick={handleClick} {...buttonProps} className='text-body-02-m text-gray-700'>
설치
</Button>

또는 더 안전하게 병합:

<Button onClick={handleClick} {...buttonProps} className={cn('text-body-02-m text-gray-700', buttonProps.className)}>

@claude

claude Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

증분 리뷰 완료

이전 지적사항 해결 여부

Import 그룹 사이 빈 줄 누락 (PWAInstallButton/index.tsx) - 해결됨
className 우선순위 이슈 (PWAInstallButton/index.tsx:29) - 미해결

새로 발견된 이슈

없음

종합

PWA 설치 기능 구현 자체는 잘 작성되었습니다:

  • usePWAInstall 훅의 SSR-safe iOS 감지 (useSyncExternalStore 활용)
  • 브라우저별 분기 (beforeinstallprompt vs iOS 수동 안내)
  • 적절한 'use client' 경계 설정 (leaf 컴포넌트에만 사용)
  • 서버/클라이언트 컴포넌트 분리 원칙 준수

다만 이전 리뷰의 className 우선순위 이슈가 아직 해결되지 않았습니다. 현재 {...buttonProps}가 inline className 뒤에 위치하여, buttonProps에 className이 포함되면 의도한 스타일이 무시됩니다.


return (
<>
<Button onClick={handleClick} {...buttonProps} className='text-body-02-m text-gray-700'>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] className 우선순위 이슈 (재지적)

현재 {...buttonProps}onClick 뒤에 오고 className이 가장 마지막에 와서, buttonProps.className이 무시됩니다.

해결 방법:

  1. spread를 먼저 배치하고 나머지를 뒤에 (권장):
Suggested change
<Button onClick={handleClick} {...buttonProps} className='text-body-02-m text-gray-700'>
<Button {...buttonProps} onClick={handleClick} className='text-body-02-m text-gray-700'>
설치
</Button>
  1. 또는 cn()으로 안전하게 병합:
<Button onClick={handleClick} {...buttonProps} className={cn('text-body-02-m text-gray-700', buttonProps.className)}>


return (
<>
<Button onClick={handleClick} {...buttonProps} className='text-body-02-m text-gray-700'>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] className 우선순위 이슈 (미해결)

현재 {...buttonProps}가 인라인 className 앞에 위치하여, 소비자가 buttonProps로 전달한 className이 무시됩니다.

JSX에서는 뒤에 오는 prop이 앞의 prop을 덮어쓰므로, 현재 코드는 항상 'text-body-02-m text-gray-700'만 적용됩니다.

권장 해결 방법:

  1. cn()으로 안전하게 병합 (가장 안전):
Suggested change
<Button onClick={handleClick} {...buttonProps} className='text-body-02-m text-gray-700'>
<Button onClick={handleClick} {...buttonProps} className={cn('text-body-02-m text-gray-700', buttonProps.className)}>
설치
</Button>
  1. 또는 소비자 className이 항상 우선하도록:
<Button {...buttonProps} onClick={handleClick} className='text-body-02-m text-gray-700'>

방법 1을 권장합니다 (기본 스타일 + 소비자 스타일 병합).

Comment on lines +3 to +8
import { useState, useSyncExternalStore } from 'react';
import { usePathname } from 'next/navigation';

import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon } from '@/components/ui/Icon';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Import 그룹 사이 빈 줄 누락

코드 스타일 규칙에 따르면 import 그룹 사이에 빈 줄이 필요합니다:

  1. React / Next.js (react, next/*)
  2. (빈 줄)
  3. 내부 모듈 (@/...)
Suggested change
import { useState, useSyncExternalStore } from 'react';
import { usePathname } from 'next/navigation';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon } from '@/components/ui/Icon';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';
import { useState, useSyncExternalStore } from 'react';
import { usePathname } from 'next/navigation';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon } from '@/components/ui/Icon';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';

Comment on lines +1 to +7
import { useEffect, useState } from 'react';

import { useIsMounted } from '@frontend-toolkit-js/hooks';

import { useRegisterFcmToken, useUnregisterFcmToken } from '@/api/notifications/queries';
import { useFCMToken } from '@/hooks/useFCMToken';
import { PUSH_NOTIFICATION_ENABLED_KEY, PUSH_NOTIFICATION_TOKEN_KEY } from '@/lib/pushNotification';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Import 그룹 사이 빈 줄 누락

코드 스타일 규칙에 따르면 import 그룹 사이에 빈 줄이 필요합니다:

  1. React / 외부 라이브러리
  2. (빈 줄)
  3. 내부 모듈 (@/...)
Suggested change
import { useEffect, useState } from 'react';
import { useIsMounted } from '@frontend-toolkit-js/hooks';
import { useRegisterFcmToken, useUnregisterFcmToken } from '@/api/notifications/queries';
import { useFCMToken } from '@/hooks/useFCMToken';
import { PUSH_NOTIFICATION_ENABLED_KEY, PUSH_NOTIFICATION_TOKEN_KEY } from '@/lib/pushNotification';
import { useEffect, useState } from 'react';
import { useIsMounted } from '@frontend-toolkit-js/hooks';
import { useRegisterFcmToken, useUnregisterFcmToken } from '@/api/notifications/queries';
import { useFCMToken } from '@/hooks/useFCMToken';
import { PUSH_NOTIFICATION_ENABLED_KEY, PUSH_NOTIFICATION_TOKEN_KEY } from '@/lib/pushNotification';

Comment on lines +3 to +8
import { useEffect } from 'react';

import { AlarmIcon, AlarmOnIcon } from '@/components/ui/Icon';
import { useToastStore } from '@/components/ui/Toast/useToastStore';
import { Toggle } from '@/components/ui/Toggle';
import { usePushNotificationSetting } from '@/hooks/usePushNotificationSetting';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Import 그룹 사이 빈 줄 누락

코드 스타일 규칙에 따르면 import 그룹 사이에 빈 줄이 필요합니다:

  1. React
  2. (빈 줄)
  3. 내부 모듈 (@/...)
Suggested change
import { useEffect } from 'react';
import { AlarmIcon, AlarmOnIcon } from '@/components/ui/Icon';
import { useToastStore } from '@/components/ui/Toast/useToastStore';
import { Toggle } from '@/components/ui/Toggle';
import { usePushNotificationSetting } from '@/hooks/usePushNotificationSetting';
import { useEffect } from 'react';
import { AlarmIcon, AlarmOnIcon } from '@/components/ui/Icon';
import { useToastStore } from '@/components/ui/Toast/useToastStore';
import { Toggle } from '@/components/ui/Toggle';
import { usePushNotificationSetting } from '@/hooks/usePushNotificationSetting';

import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon, ArrowIcon } from '@/components/ui/Icon';
import { PWAInstallButton } from '@/components/PWAInstallButton';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] 상대 경로 import가 내부 모듈과 섞여 있음

코드 스타일 규칙에 따르면 상대 경로 import(./, ../)는 내부 모듈(@/...) import 뒤에 별도 그룹으로 분리되어야 합니다.

현재 lines 8-9의 상대 경로 import가 lines 10-15의 내부 모듈 import 사이에 위치하여 순서가 섞여 있습니다.

Suggested change
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';
'use client';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { useLogout } from '@/api/auth/queries';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon, ArrowIcon } from '@/components/ui/Icon';
import { PWAInstallButton } from '@/components/PWAInstallButton';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';
import { AuthSection } from './AuthSection';
import { PWAInstallBanner } from './PWAInstallBanner';

Comment on lines 8 to +15
import { AuthSection } from './AuthSection';
import { PWAInstallBanner } from './PWAInstallBanner';
import { useAuth } from '@/hooks/useAuth';
import { useLogout } from '@/api/auth/queries';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon, ArrowIcon } from '@/components/ui/Icon';
import { PWAInstallButton } from '@/components/PWAInstallButton';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] 상대 경로 import가 내부 모듈과 섞여 있음

코드 스타일 규칙에 따르면 상대 경로 import(./, ../)는 내부 모듈(@/...) import 뒤에 별도 그룹으로 분리되어야 합니다.

현재 lines 8-9의 상대 경로 import가 lines 10-15의 내부 모듈 import 사이에 위치하여 순서가 섞여 있습니다.

Suggested change
import { AuthSection } from './AuthSection';
import { PWAInstallBanner } from './PWAInstallBanner';
import { useAuth } from '@/hooks/useAuth';
import { useLogout } from '@/api/auth/queries';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon, ArrowIcon } from '@/components/ui/Icon';
import { PWAInstallButton } from '@/components/PWAInstallButton';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';
import { useAuth } from '@/hooks/useAuth';
import { useLogout } from '@/api/auth/queries';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import { CloseIcon, ArrowIcon } from '@/components/ui/Icon';
import { PWAInstallButton } from '@/components/PWAInstallButton';
import { IOSInstallGuideBottomSheet } from '@/components/PWAInstallButton/IOSInstallGuideBottomSheet';
import { AuthSection } from './AuthSection';
import { PWAInstallBanner } from './PWAInstallBanner';

Sign up for free to 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.

[SM-209] feat: PWA 관련 UX 설계

2 participants