diff --git a/app.vue b/app.vue index 5db5c43..537d4ec 100644 --- a/app.vue +++ b/app.vue @@ -15,30 +15,21 @@ import { SW } from "./registerServiceWorker"; onMounted(() => { + // Service Worker 세팅 if ("serviceWorker" in navigator) { SW(); } - //// START - IOS APP인 경우 vh 로직 - // const userAgent = navigator.userAgent.toLowerCase(); - // if ( - // userAgent.includes("iphone") || - // userAgent.includes("ipod") || - // userAgent.includes("ipad") - // ) { - // document.documentElement.style.setProperty("--vh", "1vh"); - // } else { - //// END - // 초기 뷰포트 높이 설정 + // Viewport 세팅 let vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); - // 창 크기가 변경될 때마다 뷰포트 높이 업데이트 window.addEventListener("resize", () => { vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); }); - // } + + // Clarity 세팅 (function (c, l, a, r, i, t, y) { c[a] = c[a] || @@ -51,7 +42,18 @@ onMounted(() => { y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y); })(window, document, "clarity", "script", "jsur0l64cq"); + + // Google Tag Manager + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + })(window,document,'script','dataLayer','GTM-NJPB6T5D'); }); + +/** + * Head + */ useHeadSafe({ script: [{ id: "xss-script", innerHTML: 'alert("xss")' }], meta: [{ "http-equiv": "refresh", content: "0;javascript:alert(1)" }], @@ -65,12 +67,14 @@ useSeoMeta({ ogTitle: "나만의 기록 비서 Looi", description: `당신의 기록을 더욱 쉽고 가치있게!`, ogDescription: `당신의 기록을 더욱 쉽고 가치있게!`, - ogImage: "https://kr.object.ncloudstorage.com/looi/%EA%B7%B8%EB%9E%98%ED%94%BD%20%EF%BC%86%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%EC%9D%B4%EB%AF%B8%EC%A7%80.png", + ogImage: + "https://kr.object.ncloudstorage.com/looi/%EA%B7%B8%EB%9E%98%ED%94%BD%20%EF%BC%86%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%EC%9D%B4%EB%AF%B8%EC%A7%80.png", twitterCard: "summary_large_image", ogType: "website", ogSiteName: "나만의 기록 비서 Looi", twitterTitle: "나만의 기록 비서 Looi", twitterDescription: `당신의 기록을 더욱 쉽고 가치있게!`, - twitterImage: "https://kr.object.ncloudstorage.com/looi/%EA%B7%B8%EB%9E%98%ED%94%BD%20%EF%BC%86%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%EC%9D%B4%EB%AF%B8%EC%A7%80.png", + twitterImage: + "https://kr.object.ncloudstorage.com/looi/%EA%B7%B8%EB%9E%98%ED%94%BD%20%EF%BC%86%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%EC%9D%B4%EB%AF%B8%EC%A7%80.png", }); diff --git a/models/auth.ts b/models/auth.ts index f5dab67..62cfef6 100644 --- a/models/auth.ts +++ b/models/auth.ts @@ -52,3 +52,14 @@ export interface SocialCallbackModel { is_signup: boolean; user_name: string; } + +/** + * SignupModel + */ +export interface SignupModel { + nickname?: string; + mbti?: string; + age?: string; + gender?: string; + push_token?: string; +} diff --git a/models/diary.ts b/models/diary.ts index 55ae143..5127b6b 100644 --- a/models/diary.ts +++ b/models/diary.ts @@ -62,6 +62,7 @@ interface Diary { is_completed?: boolean; resolution?: string; main_keyword?: string; // TODO: string[] + share_id?: string; } /** diff --git a/nuxt.config.ts b/nuxt.config.ts index 11d6e97..f72c6cb 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -76,5 +76,7 @@ export default defineNuxtConfig({ mode: "client", }, "~/plugins/axios", + "~/plugins/native", // Native App 함수 호출 + "~/plugins/functions", // Receive From Native App (JavaScript Functions) ], }); diff --git a/pages/callback/apple.vue b/pages/callback/apple.vue index f52f418..01b6c4e 100644 --- a/pages/callback/apple.vue +++ b/pages/callback/apple.vue @@ -11,6 +11,8 @@ import { useUserStore } from "~/store/user"; import { useAuthService } from "../../services/auth"; +const { $native } = useNuxtApp(); + const { getSocialCallback } = useAuthService(); const route = useRoute(); const router = useRouter(); @@ -24,7 +26,7 @@ onMounted(async () => { console.log(">> apple res", res); if (res.success) { - // 로그인 성공 시, 쿠키/store 세팅 + // 로그인 성공 시, 쿠키/store 세팅, FCMToken 저장 const { setAccessToken, setRefreshToken, setUser } = useUserStore(); const now = new Date(); const accessTokenExpires = new Date( @@ -47,6 +49,9 @@ onMounted(async () => { setAccessToken(res.data.access_token); setRefreshToken(res.data.refresh_token); + // App에 FCM Token 요청 + $native.reqFCMToken(); + if (res.data.is_signup) { router.push(`/profile/starter`); return; diff --git a/pages/callback/kakao.vue b/pages/callback/kakao.vue index d11d9e3..29740ae 100644 --- a/pages/callback/kakao.vue +++ b/pages/callback/kakao.vue @@ -11,6 +11,8 @@ import { useUserStore } from "~/store/user"; import { useAuthService } from "../../services/auth"; +const { $native } = useNuxtApp(); + const { getSocialCallback } = useAuthService(); const route = useRoute(); const router = useRouter(); @@ -22,7 +24,7 @@ onMounted(async () => { const res = await getSocialCallback("kakao", route.query.code); if (res.success) { - // 로그인 성공 시, 쿠키/store 세팅 + // 로그인 성공 시, 쿠키/store 세팅, FCMToken 저장 const { setAccessToken, setRefreshToken, setUser } = useUserStore(); const now = new Date(); const accessTokenExpires = new Date( @@ -45,6 +47,9 @@ onMounted(async () => { setAccessToken(res.data.access_token); setRefreshToken(res.data.refresh_token); + // App에 FCM Token 요청 + $native.reqFCMToken(); + if (res.data.is_signup) { router.push(`/profile/starter`); return; diff --git a/pages/diary/[id].vue b/pages/diary/[id].vue index 11d5b00..0f30aed 100644 --- a/pages/diary/[id].vue +++ b/pages/diary/[id].vue @@ -329,30 +329,34 @@ export default { } }, async shareURL() { - const url = window.location.href; - // "https://docent.zip/share/${this.diary.id}?type=${this.type}" - - try { - if (!navigator?.clipboard?.writeText) - throw new Error( - "복사 기능이 제공되지 않는 브라우저입니다.", - ); - - // 클립보드에 복사 - window.navigator.clipboard - .writeText(url.replace("diary", "share")) - .then(() => { - this.$eventBus.$emit("onConfirmModal", { - title: "URL이 복사되었습니다.", - }); - }); - } catch (e) { - console.error(e); - this.$eventBus.$emit("onConfirmModal", { - title: "URL 복사에 실패하였습니다", - desc: e.message, + const baseUrl = window.location.href.split("/").slice(0, 3).join("/"); + let url = `${baseUrl}/share/${this.diary.share_id}`; + if (this.type === "1") { + url += "?type=1"; + } else if (this.type === "2") { + url += "?type=2"; + } + try { + if (!navigator?.clipboard?.writeText) + throw new Error( + "복사 기능이 제공되지 않는 브라우저입니다.", + ); + + // 클립보드에 복사 + window.navigator.clipboard + .writeText(url) + .then(() => { + this.$eventBus.$emit("onConfirmModal", { + title: "URL이 복사되었습니다.", + }); }); - } + } catch (e) { + console.error(e); + this.$eventBus.$emit("onConfirmModal", { + title: "URL 복사에 실패하였습니다", + desc: e.message, + }); + } }, handleEditMode(type) { this.isEditMode = true; diff --git a/pages/share/[id].vue b/pages/share/[id].vue index ce71f66..2bd98a2 100644 --- a/pages/share/[id].vue +++ b/pages/share/[id].vue @@ -1,7 +1,7 @@ diff --git a/plugins/functions.js b/plugins/functions.js new file mode 100644 index 0000000..7da3e6a --- /dev/null +++ b/plugins/functions.js @@ -0,0 +1,23 @@ +/** + * functions.js + * (App -> Web 앱에서 JavaScript 함수 호출) + */ + +import { useAuthService } from "~/services/auth"; + +export default defineNuxtPlugin((nuxtApp) => { + const resFCMToken = async (token) => { + console.log("✈️Hybrid Function Called: \n", token); + const res = await useAuthService().signup({ push_token: token }); + if (res.success) { + console.log("*FCM 토큰 등록 성공", res); + } + + return true; + }; + + // Native 앱에서 사용할 수 있도록 window 객체에 할당 + if (typeof window !== "undefined") { + window.resFCMToken = resFCMToken; + } +}); diff --git a/plugins/native.js b/plugins/native.js new file mode 100644 index 0000000..53259a1 --- /dev/null +++ b/plugins/native.js @@ -0,0 +1,23 @@ +/** + * native.js + * (Web -> App 웹에서 앱에 정의된 Bridge 함수 호출) + */ +import { isIOSApp } from "~/utils/utils"; + +export default defineNuxtPlugin(() => { + const native = { + reqFCMToken: () => { + console.log("Call Native bridge [ reqFCMToken ]"); + + if (isIOSApp()) { + window.webkit.messageHandlers.reqFCMToken.postMessage(""); + return; + } + + console.error("Failed Native bridge [ reqFCMToken ]"); + }, + }; + return { + provide: { native }, + }; +}); diff --git a/services/auth.ts b/services/auth.ts index 91b7739..571e340 100644 --- a/services/auth.ts +++ b/services/auth.ts @@ -4,18 +4,9 @@ import type { SocialLoginModel, SocialCallbackModel, RefreshModel, + SignupModel, } from "~/models/auth"; -/** - * @interface signupData - */ -interface signupData { - nickname: string; - mbti: string; - age: string; - gender: string; -} - export const useAuthService = () => { const { SERVER_MODE } = useRuntimeConfig().public; const { GET, POST, PUT, PATCH, DELETE } = useAxios(); @@ -46,7 +37,7 @@ export const useAuthService = () => { * 초기 정보 등록 * @body user */ - async signup(data: signupData) { + async signup(data: SignupModel) { return await PATCH(`/auth/update`, { ...data, }); diff --git a/services/diary.ts b/services/diary.ts index 3faa97e..df04338 100644 --- a/services/diary.ts +++ b/services/diary.ts @@ -108,16 +108,10 @@ export const useDiaryService = () => { }, /** - * 아침 다이어리 조회 - 공유용 (꿈) + * 공유된 게시물 조회 */ - async getShareMorningdiary(diary_id: number) { - return await GET(`/share/dream/${diary_id}`); - }, - /** - * 저녁 다이어리 조회 - 공유용 (일기) - */ - async getShareNightdiary(diary_id: number) { - return await GET(`/share/diary/${diary_id}`); + async getSharedDiary(diary_id: string) { + return await GET(`/share/${diary_id}`); }, /** diff --git a/utils/utils.js b/utils/utils.js index 234999e..529993f 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -165,3 +165,13 @@ export function isIOS() { return /iPhone|iPad|iPod/i.test(userAgent); } } + +/** + * IOS App 확인 함수 (looi-ios) + */ +export function isIOSApp() { + if (window) { + const userAgent = window.navigator.userAgent.toLowerCase(); + return /looi-ios/i.test(userAgent); + } +}