Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions frontend/src/app/(login)/login/_components/ServiceIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,34 +121,34 @@ export default function ServiceIntro() {
>
<item.icon className="w-5 h-5" />
</span>
<p className="text-base md:text-lg lg:text-xl font-bold mb-2 text-itta-point">
<p className="text-lg lg:text-xl font-bold mb-2 text-itta-point">
{item.caption}
</p>
<h3
className={cn(
'text-xl md:text-2xl lg:text-4xl font-bold leading-[1.35] whitespace-pre-line mb-4',
'text-2xl lg:text-4xl font-bold leading-[1.35] whitespace-pre-line mb-4',
isLight ? 'text-itta-black' : 'text-white',
)}
>
{item.headline}
</h3>
<p
className={cn(
'text-sm md:text-base lg:text-lg leading-relaxed whitespace-pre-line',
'text-base lg:text-lg leading-relaxed whitespace-pre-line',
isLight ? 'text-gray-500' : 'text-white/60',
)}
>
{item.description}
</p>
</div>

<div className="w-full max-w-40 md:max-w-65 lg:max-w-72 shrink-0">
<div className="w-full max-w-65 lg:max-w-72 shrink-0">
<Image
src={item.image}
alt={item.caption}
width={512}
height={1001}
sizes="(min-width: 1024px) 288px, (min-width: 768px) 260px, 160px"
sizes="(min-width: 1024px) 288px, 260px"
className="w-full h-auto object-contain"
/>
</div>
Expand All @@ -167,14 +167,14 @@ export default function ServiceIntro() {
<span className="inline-flex items-center justify-center w-11 h-11 rounded-full bg-gray-100 text-itta-black mb-5">
<Book className="w-5 h-5" />
</span>
<p className="text-base md:text-lg lg:text-xl font-bold text-itta-point mb-2">
<p className="text-lg lg:text-xl font-bold text-itta-point mb-2">
보관함
</p>
<h3 className="text-xl md:text-2xl lg:text-4xl font-bold leading-[1.35] mb-4 text-itta-black">
<h3 className="text-2xl lg:text-4xl font-bold leading-[1.35] mb-4 text-itta-black">
그룹마다 따로,
<br />또 같이 쌓이는 우리의 기록
</h3>
<p className="text-sm md:text-base lg:text-lg leading-relaxed text-gray-500">
<p className="text-base lg:text-lg leading-relaxed text-gray-500">
가족 여행부터 친구들과의 일상까지
<br />
그룹별로 나누어 보관함에 기록해요
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/app/(main)/_components/homeCoachmarkSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ export const HOME_COACHMARK_STEPS: CoachmarkStep[] = [
{
id: 'tutorial-nav-my',
title: '개인 기록과 그룹 기록을 나눠서 관리해요',
description: '나만 보는 기록은 개인 기록함에, 함께 쓰는 기록은 그룹에 따로 쌓여요.',
description:
'나만 보는 기록은 개인 기록함에, 함께 쓰는 기록은 그룹에 따로 쌓여요.',
yOffsetMobile: 4,
isBottomNavTarget: true,
},
{
id: 'tutorial-fab-add-record',
title: '여기서 기록을 남겨보세요',
description: '사진과 함께 오늘의 순간을 기록해요.',
yOffsetMobile: 6.2,
isBottomNavTarget: true,
},
{
id: 'tutorial-nav-group',
title: '그룹으로 함께 기록해요',
description: '친구를 초대해서 함께 추억을 쌓아보세요.',
yOffsetMobile: 4,
isBottomNavTarget: true,
},
];
30 changes: 27 additions & 3 deletions frontend/src/app/(main)/profile/_components/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { usePWAInstall } from '@/hooks/usePWAInstall';
import PWAInstallModal from '@/components/PWAInstallModal';
import { useAuthStore } from '@/store/useAuthStore';
Expand Down Expand Up @@ -56,6 +56,9 @@ export default function Setting() {
const [showInstructions, setShowInstructions] = useState(false);
const [pushEnabled, setPushEnabled] = useState(false);
const [pushBlocked, setPushBlocked] = useState(false);
// 연속으로 토글했을 때 먼저 시작된(느린) 요청이 나중에 끝나면서 최신 상태를
// 덮어쓰지 않도록, 매 호출마다 증가시켜 "이 완료가 아직 최신 요청인지" 판별한다.
const pushRequestIdRef = useRef(0);
Comment thread
H-sooyeon marked this conversation as resolved.

const { mutate: logout } = useApiPost('/api/auth/logout', {
onSuccess: async () => {
Expand Down Expand Up @@ -143,17 +146,32 @@ export default function Setting() {
};

const handleTogglePush = async () => {
const requestId = ++pushRequestIdRef.current;
const isStale = () => pushRequestIdRef.current !== requestId;
Comment thread
H-sooyeon marked this conversation as resolved.
const platform = isNativePlatform() ? 'android' : 'web';

if (pushEnabled) {
// 낙관적 업데이트: 먼저 꺼진 것으로 표시하고, 실패하면 되돌린다.
// 낙관적 업데이트: 먼저 꺼진 것으로 표시하고, 서버 토큰 삭제가 실패하면 되돌린다.
setPushEnabled(false);
try {
await removeFcmToken(platform);
localStorage.setItem('push_notifications_disabled', 'true');
} catch {
if (isStale()) return;
setPushEnabled(true); // 롤백
toast.error('알림 해제에 실패했습니다. 잠시 후 다시 시도해 주세요.');
return;
}
if (isStale()) return; // 그 사이 더 최신 토글이 시작됨 — 이 완료는 무시
try {
localStorage.setItem('push_notifications_disabled', 'true');
} catch (error) {
// 서버 토큰 삭제는 이미 성공했으므로 토글은 되돌리지 않는다.
Sentry.captureException(error, {
tags: {
context: 'notification',
operation: 'push-disable-persist',
},
});
}
return;
}
Expand All @@ -166,6 +184,7 @@ export default function Setting() {
const { PushNotifications } =
await import('@capacitor/push-notifications');
const { receive } = await PushNotifications.checkPermissions();
if (isStale()) return;
if (receive === 'granted') {
localStorage.removeItem('push_notifications_disabled');
setPushBlocked(false);
Expand All @@ -180,6 +199,7 @@ export default function Setting() {
}
} else {
await requestAndRegisterWebToken();
if (isStale()) return;
if (Notification.permission === 'granted') {
localStorage.removeItem('push_notifications_disabled');
setPushBlocked(false);
Expand All @@ -192,7 +212,11 @@ export default function Setting() {
}
}
} catch (error) {
if (isStale()) return;
setPushEnabled(false); // 롤백
try {
localStorage.setItem('push_notifications_disabled', 'true');
} catch {}
Comment thread
H-sooyeon marked this conversation as resolved.
Outdated
Sentry.captureException(error, {
tags: {
context: 'notification',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ export const GROUP_DETAIL_COACHMARK_STEPS: CoachmarkStep[] = [
title: '이 그룹에 기록을 남겨보세요',
description: '사진과 함께 그룹원들과의 순간을 기록해요.',
yOffsetMobile: 6.2,
isBottomNavTarget: true,
},
{
id: 'tutorial-group-map',
title: '지도에서 한눈에 확인해요',
description: '그룹의 기록들이 남긴 장소를 지도로 둘러보세요.',
yOffsetMobile: 4,
isBottomNavTarget: true,
},
];
13 changes: 3 additions & 10 deletions frontend/src/components/Coachmark.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { useLayoutEffect, useRef, useState } from 'react';
import { getNativeStatusBarOffset } from '@/lib/utils/nativeStatusBar';
import { createPortal } from 'react-dom';
import { motion } from 'framer-motion';
import { PointerIcon } from 'lucide-react';
Expand Down Expand Up @@ -36,8 +35,6 @@ export default function Coachmark({ flowKey, steps, enabled }: CoachmarkProps) {

const textRef = useRef<HTMLDivElement>(null);
const [textRect, setTextRect] = useState<DOMRect | null>(null);
// 세션 동안 바뀌지 않는 값이라 마운트 시 한 번만 계산한다.
const [nativeStatusBarOffset] = useState(getNativeStatusBarOffset);

// 텍스트 블록이 실제로 렌더된 높이를 측정해서, 화살표가 텍스트를 가리지 않고
// 정확히 텍스트가 끝나는 지점부터 시작하도록 한다(추정치 대신 실측값 사용).
Expand Down Expand Up @@ -67,11 +64,7 @@ export default function Coachmark({ flowKey, steps, enabled }: CoachmarkProps) {
),
) - (isMobileWidth ? MOBILE_PADDING_REDUCTION : 0);
// 스텝마다 필요한 경우에만(예: BottomNavigation 아이템) 모바일 폭에서 살짝 내리거나 옆으로 옮긴다.
// BottomNavigation 타겟은 Capacitor 네이티브에서 헤더와 동일한 상태바 높이만큼 추가로 내려준다
// (헤더 중복 padding 제거 시 상쇄한 값과 같은 양 — 9ce79a92 참고).
const yOffset =
(isMobileWidth ? (step.yOffsetMobile ?? 0) : 0) +
(step.isBottomNavTarget ? nativeStatusBarOffset : 0);
const yOffset = isMobileWidth ? (step.yOffsetMobile ?? 0) : 0;
const xOffset = isMobileWidth ? (step.xOffsetMobile ?? 0) : 0;
// 화면 밖으로 나가지 않도록 뷰포트 안으로 먼저 clamp한 다음, offset은 그 위에 더한다.
// 화면 가장자리에 붙은 타겟은 offset을 clamp 전에 더하면 "화면 밖으로 안 나가게"
Expand All @@ -95,8 +88,8 @@ export default function Coachmark({ flowKey, steps, enabled }: CoachmarkProps) {
window.innerWidth - rawSpotlight.width - VIEWPORT_MARGIN,
) + xOffset,
// yOffset을 더한 뒤에도 화면 밖으로 나가지 않도록 다시 한 번 clamp한다.
// nativeStatusBarOffset처럼값이 더해지면 이미 하단 가장자리에 있는
// 타겟(BottomNavigation)이 뷰포트 밖으로 밀려날 수 있어서다.
// yOffsetMobile이스텝은 이미 하단 가장자리에 있는 타겟(BottomNavigation)을
// 뷰포트 밖으로 밀어낼 수 있어서다.
top: Math.min(
Math.max(clampedTop + yOffset, VIEWPORT_MARGIN),
maxSpotlightTop,
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/hooks/useCoachmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ export interface CoachmarkStep {
xOffsetMobile?: number;
// 타겟 비율 기반 자동 여백 대신 고정 여백을 쓰고 싶을 때(예: 아이콘 하나만 딱 감싸기).
spotlightPadding?: number;
// BottomNavigation 안의 타겟(NavItem, FAB 등)에만 사용.
// Capacitor 네이티브 환경에서 헤더 중복 padding 제거 시 상쇄한 것과 동일한
// 상태바 높이(--cap-status-bar-height/env(safe-area-inset-top))만큼
// 스포트라이트를 추가로 내려준다.
isBottomNavTarget?: boolean;
}

interface UseCoachmarkOptions {
Expand Down
23 changes: 0 additions & 23 deletions frontend/src/lib/utils/nativeStatusBar.ts

This file was deleted.

18 changes: 9 additions & 9 deletions mobile-app/ios/App/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ PODS:
- CapacitorCordova (7.5.0)
- CapacitorHaptics (7.0.5):
- Capacitor
- CapacitorNativeSettings (8.1.0):
- CapacitorNativeSettings (7.0.2):
- Capacitor
- CapacitorPushNotifications (8.1.1):
- CapacitorPushNotifications (7.0.7):
- Capacitor
- CapacitorShare (7.0.4):
- Capacitor
Expand All @@ -28,8 +28,8 @@ DEPENDENCIES:
- "CapacitorClipboard (from `../../../node_modules/.pnpm/@capacitor+clipboard@7.0.4_@capacitor+core@7.5.0/node_modules/@capacitor/clipboard`)"
- "CapacitorCordova (from `../../../node_modules/.pnpm/@capacitor+ios@7.5.0_@capacitor+core@7.5.0/node_modules/@capacitor/ios`)"
- "CapacitorHaptics (from `../../../node_modules/.pnpm/@capacitor+haptics@7.0.5_@capacitor+core@7.5.0/node_modules/@capacitor/haptics`)"
- "CapacitorNativeSettings (from `../../../node_modules/.pnpm/capacitor-native-settings@8.1.0_@capacitor+core@7.5.0/node_modules/capacitor-native-settings`)"
- "CapacitorPushNotifications (from `../../../node_modules/.pnpm/@capacitor+push-notifications@8.1.1_@capacitor+core@7.5.0/node_modules/@capacitor/push-notifications`)"
- "CapacitorNativeSettings (from `../../../node_modules/.pnpm/capacitor-native-settings@7.0.2_@capacitor+core@7.5.0/node_modules/capacitor-native-settings`)"
- "CapacitorPushNotifications (from `../../../node_modules/.pnpm/@capacitor+push-notifications@7.0.7_@capacitor+core@7.5.0/node_modules/@capacitor/push-notifications`)"
- "CapacitorShare (from `../../../node_modules/.pnpm/@capacitor+share@7.0.4_@capacitor+core@7.5.0/node_modules/@capacitor/share`)"
- "CapacitorSplashScreen (from `../../../node_modules/.pnpm/@capacitor+splash-screen@7.0.5_@capacitor+core@7.5.0/node_modules/@capacitor/splash-screen`)"
- "CapacitorStatusBar (from `../../../node_modules/.pnpm/@capacitor+status-bar@7.0.6_@capacitor+core@7.5.0/node_modules/@capacitor/status-bar`)"
Expand All @@ -48,9 +48,9 @@ EXTERNAL SOURCES:
CapacitorHaptics:
:path: "../../../node_modules/.pnpm/@capacitor+haptics@7.0.5_@capacitor+core@7.5.0/node_modules/@capacitor/haptics"
CapacitorNativeSettings:
:path: "../../../node_modules/.pnpm/capacitor-native-settings@8.1.0_@capacitor+core@7.5.0/node_modules/capacitor-native-settings"
:path: "../../../node_modules/.pnpm/capacitor-native-settings@7.0.2_@capacitor+core@7.5.0/node_modules/capacitor-native-settings"
CapacitorPushNotifications:
:path: "../../../node_modules/.pnpm/@capacitor+push-notifications@8.1.1_@capacitor+core@7.5.0/node_modules/@capacitor/push-notifications"
:path: "../../../node_modules/.pnpm/@capacitor+push-notifications@7.0.7_@capacitor+core@7.5.0/node_modules/@capacitor/push-notifications"
CapacitorShare:
:path: "../../../node_modules/.pnpm/@capacitor+share@7.0.4_@capacitor+core@7.5.0/node_modules/@capacitor/share"
CapacitorSplashScreen:
Expand All @@ -65,12 +65,12 @@ SPEC CHECKSUMS:
CapacitorClipboard: 9779d5e2f766d501d8c90e0f0f689abd293efc2f
CapacitorCordova: ae35646a9b46cfd00d637f195509c86594026aad
CapacitorHaptics: ec024a10130c230743219149c76a02da5cff6aed
CapacitorNativeSettings: 3eb6d229080d11431c7126c7aa241cd05ec3ff23
CapacitorPushNotifications: 7fa75d28307b23b314b19ec7cfe88611e9659352
CapacitorNativeSettings: f5511eb94cb5700ac4edf54f992d2c0dda1857d6
CapacitorPushNotifications: f7e0909b16354405bbf4fd50b1ae5111843220ab
CapacitorShare: 14b5d7f0a2c6de86dcad8995da80fb707ee32f62
CapacitorSplashScreen: b3d12b4324d93ce605fe57f05a199ade7f26189e
CapacitorStatusBar: a3acb591ff56301e28aa1a01791ba74a8e1faabb

PODFILE CHECKSUM: a725e27d0846ecb14295bdce707ad26b0908fffd
PODFILE CHECKSUM: c6e27cb4fc0ac69024f2d0387b9f527cf50e784b

COCOAPODS: 1.16.2
Loading