Skip to content
Merged
Binary file added public/images/share-kakao.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/share-link.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ export async function generateMetadata(): Promise<Metadata> {
}

export default function RootPage() {
redirect('/project');
redirect('/teampsylog');
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const KeywordListPage = () => {

return (
<>
<KeywordPage share={true} uuid={uuid} />
<KeywordPage uuid={uuid} />
</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/app/(main)/teampsylog/_components/CommentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ const CommentPage = ({

return (
<div
className="desktop:gap-7 desktop:p-8 scrollbar-hide flex flex-col gap-3 px-4 pt-3 pb-5"
className="desktop:gap-5 desktop:p-8 scrollbar-hide flex flex-col gap-3 px-4 pt-3 pb-5"
style={{ overflowY: 'auto' }}
>
<div className="flex flex-col">
<p className="desktop:title-4 body-7">{`‘${keywordName}’에 대한 다른 사람들의 코멘트에요`}</p>
<p className="desktop:body-6 body-9 text-gray-600">중복된 키워드는 말풍선이 커져요</p>
{/* <p className="desktop:body-6 body-9 text-gray-600">중복된 키워드는 말풍선이 커져요</p> */}
</div>
<div className="desktop:gap-3 flex flex-col gap-2">
{comments.map((comment, idx) => (
Expand Down
7 changes: 6 additions & 1 deletion src/app/(main)/teampsylog/_components/KeywordBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProfileDropdown from './ProfileDropdown';
import { ResponseProfile } from '@/types/profile';
import { useToast } from '@/contexts/ToastContext';
import KeywordGuideBalloon from './KeywordGuideBalloon';
import { useModal } from '@/contexts/ModalContext';

const KeywordBar = ({
profileId,
Expand Down Expand Up @@ -42,6 +43,8 @@ const KeywordBar = ({
...Array(Math.max(0, 3 - headKeywords.length)).fill('키워드 선택'),
];

// 모달 추가
const { openModal } = useModal();
const handleShare = async () => {
let uuid: string | undefined;
if (typeof window !== 'undefined') {
Expand All @@ -60,7 +63,9 @@ const KeywordBar = ({
? `${window.location.origin}/teampsylog/head/${uuid}`
: `${window.location.origin}/teampsylog`;
await navigator.clipboard.writeText(url);
addToast({ message: '링크가 복사되었어요' });

openModal('linkShare');
// addToast({ message: '링크가 복사되었어요' });
Comment on lines 65 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

클립보드 쓰기 실패 시 공유 플로우가 중단됩니다.

navigator.clipboard.writeText 실패(권한/브라우저 정책) 시 예외가 전파되어 openModal('linkShare')가 실행되지 않습니다. try/catch로 실패 토스트 또는 폴백 처리가 필요합니다.

수정 예시
-    await navigator.clipboard.writeText(url);
-
-    openModal('linkShare');
+    try {
+      await navigator.clipboard.writeText(url);
+      openModal('linkShare');
+    } catch {
+      addToast({
+        type: 'error',
+        title: '링크 복사에 실패했어요.',
+        message: '브라우저 권한을 확인해주세요.',
+      });
+    }
🤖 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 `@src/app/`(main)/teampsylog/_components/KeywordBar.tsx around lines 65 - 68,
Wrap the navigator.clipboard.writeText(url) call in a try/catch inside the
KeywordBar component so a rejected promise doesn’t block the sharing flow: call
navigator.clipboard.writeText(url) inside try, and in catch call addToast (or
the existing toast helper) with a failure message and/or fallback behavior, but
always proceed to call openModal('linkShare') after the attempt; reference the
navigator.clipboard.writeText call, the openModal('linkShare') invocation, and
addToast (currently commented) when implementing the change.

};

const [dismissedByUser, setDismissedByUser] = useState(false);
Expand Down
15 changes: 9 additions & 6 deletions src/app/(main)/teampsylog/_components/KeywordPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ import Loading from '@/components/common/Loading';
import KeywordGuideOverlay from './KeywordGuideOverlay';

interface Props {
share?: boolean;
uuid?: string;
}

const KeywordPage = ({ share = false, uuid }: Props) => {
// uuid 비교후 사용자와 동일하면 일반모드, 다르면 공유모드.

const KeywordPage = ({ uuid }: Props) => {
const router = useRouter();
const { addToast } = useToast();
const { userName, _hasHydrated } = useUserStore();
const { uuid: userUuid, userName, _hasHydrated } = useUserStore();

const share = !_hasHydrated || userUuid !== uuid;

const hasAlerted = useRef(false);

Expand Down Expand Up @@ -53,14 +56,14 @@ const KeywordPage = ({ share = false, uuid }: Props) => {
}
}, [_hasHydrated, userName, router, addToast, share]);

// 공유 모드: uuid가 있을 때만 실행
// 공유 모드일 때만 실행
const requesterInfoResult = useRequesterInfo(uuid ?? '', { enabled: share && !!uuid });
const requesterInfo = requesterInfoResult.data;
const shareUserId = share && requesterInfo ? requesterInfo.userId : null;
const requesterName = requesterInfo?.requesterName;

// 공유모드 getProfileList 가져오기
const { data: sharedProfiles } = useGetUuidProfileList(uuid ?? '');
// 공유 모드일 때만 uuid 기반 프로필을 가져온다.
const { data: sharedProfiles } = useGetUuidProfileList(share && uuid ? uuid : '');

// 일반 모드
const isAuthenticated = _hasHydrated && checkIsLoggedIn(userName);
Expand Down
27 changes: 23 additions & 4 deletions src/app/(main)/teampsylog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import MobileHeader from '@/components/common/MobileHeader';
import KeywordPage from './_components/KeywordPage';
'use client';
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import Loading from '@/components/common/Loading';
import { useUserStore } from '@/store/useUserStore';

const Page = () => {
const router = useRouter();
const { uuid, _hasHydrated } = useUserStore();
const redirectedRef = useRef(false);

useEffect(() => {
if (!_hasHydrated || redirectedRef.current) return;

redirectedRef.current = true;

if (uuid) {
router.replace(`/teampsylog/${uuid}`);
return;
}

router.replace('/login');
}, [_hasHydrated, router, uuid]);

return (
<section className="h-dvh">
<MobileHeader title="팀피셜록" />
<KeywordPage />
<Loading />
</section>
);
};
Expand Down
8 changes: 6 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import Script from 'next/script';
import { GoogleTagManager } from '@next/third-parties/google';
import './globals.css';
import { ModalProvider } from '@/contexts/ModalContext';
import Providers from './provider';
import { ToastProvider } from '@/contexts/ToastContext';
import KakaoScript from '@/components/common/KaKaoScript';

const pretendard = localFont({
src: '../../public/fonts/PretendardVariable.woff2',
Expand All @@ -15,13 +17,14 @@ const pretendard = localFont({

export const metadata: Metadata = {
title: 'TEAMFICIAL',
description: 'Generated by create next app',
description: '소프트스킬 팀빌딩 서비스, 팀피셜',
icons: {
icon: [
{ url: '/favicon.ico', sizes: '48x48', type: 'image/x-icon' },
{ url: '/favicon.svg', type: 'image/svg+xml' },
],
apple: '/apple-touch-icon.png',
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180' }],
shortcut: '/favicon.ico',
},
};

Expand All @@ -39,6 +42,7 @@ export default function RootLayout({
</ToastProvider>
</Providers>
<GoogleTagManager gtmId="GTM-5KPSS9WV" />
<KakaoScript />
</body>
</html>
);
Expand Down
14 changes: 7 additions & 7 deletions src/components/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const Header = () => {
height={41}
/>
</Link>
<Link
{/* <Link
href="/project"
className={clsx(
'hover:text-primary-900 px-3 transition-colors',
Expand All @@ -93,7 +93,7 @@ const Header = () => {
)}
>
팀피셜록
</Link>
</Link> */}
</div>
{isLoggedIn ? (
<div className="relative">
Expand All @@ -106,7 +106,7 @@ const Header = () => {
<Image src="/icons/profile.svg" alt="profile" width={44} height={44} />
</button>
{isProfileDropdownOpen && (
<div className="body-5 absolute right-0 z-20 mt-2 w-34 cursor-pointer rounded-lg border border-gray-300 bg-white text-gray-800 shadow-lg">
<div className="body-5 absolute right-0 z-60 mt-2 w-34 cursor-pointer rounded-lg border border-gray-300 bg-white text-gray-800 shadow-lg">
<Link
href="/mypage"
className="block border-b border-gray-300 py-3 pl-7 hover:bg-gray-100"
Expand Down Expand Up @@ -187,15 +187,15 @@ const Header = () => {
</div>
<div className="mt-4 h-[1px] w-full bg-gray-300" />
<div className="body-5 flex flex-col gap-4 pt-5">
<Link
{/* <Link
href="/project"
className={clsx(
pathname.startsWith('/project') ? 'text-primary-900' : 'text-gray-800',
)}
onClick={() => setIsMenuOpen(false)}
>
프로젝트
</Link>
</Link> */}
<Link
href="/teampsylog"
className={clsx(
Expand All @@ -205,7 +205,7 @@ const Header = () => {
>
팀피셜록
</Link>
{isLoggedIn ? (
{/* {isLoggedIn ? (
<Link
href="/mypage"
className={clsx(
Expand All @@ -215,7 +215,7 @@ const Header = () => {
>
마이페이지
</Link>
) : null}
) : null} */}
</div>
</div>
<div className="mt-auto">
Expand Down
19 changes: 19 additions & 0 deletions src/components/common/KaKaoScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import Script from 'next/script';

export default function KakaoScript() {
return (
<Script
src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.2/kakao.min.js"
integrity="sha384-TiCUE00h649CAMonG018J2ujOgDKW/kVWlChEuu4jK2vxfAAD0eZxzCKakxg55G4"
crossOrigin="anonymous"
strategy="lazyOnload"
onLoad={() => {
if (window.Kakao && !window.Kakao.isInitialized()) {
window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY!);
}
Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines
FILE="src/components/common/KaKaoScript.tsx"
if [ ! -f "$FILE" ]; then
  echo "MISSING_FILE:$FILE"
  exit 0
fi

echo "== $FILE =="
nl -ba "$FILE" | sed -n '1,120p'

Repository: TEAMFICIAL/teamficial-fe

Length of output: 174


🌐 Web query:

Kakao JavaScript SDK init method parameter requirements window.Kakao.init undefined behavior

💡 Result:

Kakao.init(...) parameter requirements (JS SDK)

  • You must pass your app’s JavaScript key as a string (from [App] → [Platform key] → [JavaScript key]). The official guide shows Kakao.init("JAVASCRIPT_KEY") and then Kakao.isInitialized() to verify success. (developers.kakao.com)
  • Your site domain must be registered under JavaScript SDK domain for that JavaScript key; otherwise requests from other domains are rejected. (developers.kakao.com)

What happens if you call Kakao.init(undefined) (or a non-string / placeholder)?

  • The SDK throws an error like: KakaoError: Kakao.init: App key must be provided. A Kakao DevTalk response notes this happens when the key is not a string (or you left a placeholder like 'YOUR APP KEY'). (devtalk.kakao.com)

When window.Kakao (or window.Kakao.init) is undefined

This typically means the SDK object was never available in that runtime, e.g.:

  • The Kakao JS SDK script didn’t load / couldn’t be referenced, leading to errors like undefined is not an object (evaluating 'window.Kakao.isInitialized'); Kakao staff指出 this looks like “cannot reference the JS SDK” (script not loaded, earlier script error, or the object got overwritten). (devtalk.kakao.com)
  • SSR / non-browser execution (e.g., Next.js server render): window doesn’t exist, so window.Kakao can’t exist until client-side.

Practical guard (prevents “undefined” + catches missing key)

if (typeof window !== "undefined" && window.Kakao && !window.Kakao.isInitialized()) {
  if (!process.env.NEXT_PUBLIC_KAKAO_JS_KEY) throw new Error("Missing Kakao JS key");
  window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
}

If you share your exact script tag + where you call init() (and whether you’re using SSR), I can pinpoint which of the above is causing your undefined case.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/components/common/KaKaoScript.tsx"

echo "== cat $FILE (first 80 lines) =="
sed -n '1,80p' "$FILE" | cat -n

echo
echo "== grep Kakao.init usages =="
rg -n "Kakao\.init|NEXT_PUBLIC_KAKAO_JS_KEY" "$FILE" || true

Repository: TEAMFICIAL/teamficial-fe

Length of output: 880


환경 변수 미설정 시 Kakao 초기화가 깨지지 않도록 init 호출 가드 추가 필요

  • src/components/common/KaKaoScript.tsx에서 onLoadwindow.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY!)로 키를 단정하고 있어, 키가 비어/미설정이면 SDK가 초기화에 실패할 수 있습니다(“App key must be provided” 류 오류).
수정 예시
      onLoad={() => {
-        if (window.Kakao && !window.Kakao.isInitialized()) {
-          window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY!);
+        const kakaoKey = process.env.NEXT_PUBLIC_KAKAO_JS_KEY;
+        if (window.Kakao && !window.Kakao.isInitialized() && kakaoKey) {
+          window.Kakao.init(kakaoKey);
         }
       }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (window.Kakao && !window.Kakao.isInitialized()) {
window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY!);
}
onLoad={() => {
const kakaoKey = process.env.NEXT_PUBLIC_KAKAO_JS_KEY;
if (window.Kakao && !window.Kakao.isInitialized() && kakaoKey) {
window.Kakao.init(kakaoKey);
}
}}
🤖 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 `@src/components/common/KaKaoScript.tsx` around lines 13 - 15, The Kakao SDK
init call in KaKaoScript.tsx currently uses a non-null assertion on
process.env.NEXT_PUBLIC_KAKAO_JS_KEY and will throw if the env var is missing;
change the onLoad logic to first check that process.env.NEXT_PUBLIC_KAKAO_JS_KEY
(or a runtime variable passed into the component) is a non-empty string before
calling window.Kakao.init, skip/init only when window.Kakao exists and
!window.Kakao.isInitialized(), and optionally emit a console.warn or
processLogger message when the key is not set to make the failure visible
without breaking the app; reference window.Kakao and the init call in
KaKaoScript.tsx to locate the change.

}}
/>
);
}
83 changes: 83 additions & 0 deletions src/components/modal/LinkShareModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { BaseModalProps } from '@/constants/ModalList';
import BaseModal from '.';
import Image from 'next/image';
import { useToast } from '@/contexts/ToastContext';
import { useUserStore } from '@/store/useUserStore';

const LinkShareModal = ({ isOpen, onClose }: BaseModalProps) => {
const { addToast } = useToast();
const { userName } = useUserStore();

const getShareUrl = () => {
const uuid = window.location.pathname.split('/').pop();
return `${window.location.origin}/teampsylog/head/${uuid}`;
};
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

공유 URL 경로가 라우팅 계약과 불일치할 가능성이 큽니다.

Line 13에서 /teampsylog/head/${uuid}를 만들고 있는데, 현재 PR 목표는 UUID 기반 /teampsylog/:uuid 진입입니다. 이대로면 공유 링크가 잘못된 경로를 가리킬 수 있습니다.

수정 예시
  const getShareUrl = () => {
    const uuid = window.location.pathname.split('/').pop();
-   return `${window.location.origin}/teampsylog/head/${uuid}`;
+   return `${window.location.origin}/teampsylog/${uuid}`;
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getShareUrl = () => {
const uuid = window.location.pathname.split('/').pop();
return `${window.location.origin}/teampsylog/head/${uuid}`;
};
const getShareUrl = () => {
const uuid = window.location.pathname.split('/').pop();
return `${window.location.origin}/teampsylog/${uuid}`;
};
🤖 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 `@src/components/modal/LinkShareModal.tsx` around lines 11 - 14, The
getShareUrl function builds a share link that currently points to
/teampsylog/head/${uuid} which mismatches the agreed routing of
/teampsylog/:uuid; update getShareUrl to construct the URL using the UUID only
(i.e., change the path to /teampsylog/${uuid}), keep UUID extraction via
window.location.pathname.split('/').pop(), and ensure the returned value uses
window.location.origin combined with the corrected path so shared links hit the
proper route.


const handleKakaoShare = () => {
if (!window.Kakao?.isInitialized()) {
addToast({ message: '카카오톡 공유를 불러오는 중입니다. 잠시 후 다시 시도해주세요.' });
return;
}

const shareUrl = getShareUrl();

window.Kakao.Share.sendDefault({
objectType: 'feed',
content: {
title: '팀피셜 (Teamficial)',
description: `${userName}님이 협업 후기를 기다리고 있어요!`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

userName null 케이스를 처리해주세요.

Line 28은 userName이 null일 때 null님... 문구가 노출됩니다. 기본 문구 fallback을 두는 게 안전합니다.

수정 예시
-        description: `${userName}님이 협업 후기를 기다리고 있어요!`,
+        description: `${userName ?? '팀원'}님이 협업 후기를 기다리고 있어요!`,
🤖 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 `@src/components/modal/LinkShareModal.tsx` at line 28, The description string
in LinkShareModal uses userName as-is, which yields "null님..." when userName is
null; update the description generation (where description: `${userName}님이 협업
후기를 기다리고 있어요!` is set) to guard against null/undefined by providing a fallback
display name (e.g., use a default like '사용자' or '누군가' or a localized fallback)
or conditionally render an alternative sentence when userName is missing so the
UI never shows "null님".

imageUrl: `https://www.teamficial.com/og/Teamficial_metatag_Image.jpg`,
link: {
mobileWebUrl: shareUrl,
webUrl: shareUrl,
},
},
buttons: [
{
title: '팀피셜록 작성하기',
link: {
mobileWebUrl: shareUrl,
webUrl: shareUrl,
},
},
],
});
};

const handleCopyLink = async () => {
const shareUrl = getShareUrl();
await navigator.clipboard.writeText(shareUrl);
addToast({ message: '링크가 복사되었습니다.' });
};

return (
<BaseModal isOpen={isOpen} onClose={onClose}>
<div className="desktop:w-115 desktop:pb-0 flex flex-col pb-2">
<div className="border-b border-b-gray-300 pb-2">
<p className="body-7 desktop:body-1 text-gray-800">팀피셜록 공유하기</p>
<p className="body-10 desktop:body-4 text-gray-700">
사람들에게 나의 소프트스킬 역량을 어필하세요
</p>
</div>
<div className="desktop:gap-7 flex items-center justify-center gap-5 pt-4">
<button
onClick={handleKakaoShare}
className="desktop:gap-2 flex flex-col items-center gap-1"
>
<Image src="/images/share-kakao.png" width={48} height={48} alt="카카오톡 공유하기" />
<p className="body-10 desktop:body-5 text-gray-700">카카오톡</p>
</button>
<button
onClick={handleCopyLink}
className="desktop:gap-2 flex flex-col items-center gap-1"
>
<Image src="/images/share-link.png" width={48} height={48} alt="링크 복사" />
<p className="body-10 desktop:body-5 text-gray-700">링크복사</p>
</button>
</div>
</div>
</BaseModal>
);
};

export default LinkShareModal;
3 changes: 3 additions & 0 deletions src/constants/ModalList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { RequestReportComment } from '@/types/teampsylog';
import ReportCommentModal from '@/components/modal/ReportCommentModal';
import ReportCompleteModal from '@/components/modal/ReportCompleteModal';
import ReportErrorModal from '@/components/modal/ReportErrorModal';
import LinkShareModal from '@/components/modal/LinkShareModal';

export interface BaseModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -128,6 +129,7 @@ export const MODAL_COMPONENTS = {
reportComment: ReportCommentModal,
reportComplete: ReportCompleteModal,
reportError: ReportErrorModal,
linkShare: LinkShareModal,
};

export type ModalType = keyof typeof MODAL_COMPONENTS;
Expand All @@ -152,4 +154,5 @@ export interface ModalPropsMap {
reportComment: ReportCommentModalProps;
reportComplete: ReportCompleteModalProps;
reportError: BaseModalProps;
linkShare: BaseModalProps;
}
1 change: 1 addition & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.css';
29 changes: 29 additions & 0 deletions src/types/kakao.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
interface Window {
Kakao: {
isInitialized: () => boolean;
init: (key: string) => void;
Share: {
sendDefault: (settings: KakaoShareSettings) => void;
};
};
}

interface KakaoShareSettings {
objectType: 'feed' | 'list' | 'location' | 'commerce' | 'text';
content: {
title: string;
description?: string;
imageUrl?: string;
link: {
mobileWebUrl?: string;
webUrl?: string;
};
};
buttons?: {
title: string;
link: {
mobileWebUrl?: string;
webUrl?: string;
};
}[];
}
Loading