EXITN

카카오톡 문의 위젯

카카오 채널 문의 위젯을 설치형 확장팩으로 추가하는 방법

카카오톡 문의 위젯

카카오 문의 버튼과 SDK 연결을 한 번에 붙이는 확장팩입니다.

설치

pnpm dlx shadcn@latest add https://n-exit.io/r/kakao-channel.json
npx shadcn@latest add https://n-exit.io/r/kakao-channel.json

추가 파일

floating-kakao-chat-button.tsx
kakao-channel-provider.tsx
kakao-channel.ts
kakao.d.ts
"use client";

import type { ReactNode } from "react";
import { useEffect } from "react";
import FloatingKakaoChatButton from "@/components/floating-kakao-chat-button";
import { initializeKakaoChannelSdk } from "@/services/kakao-channel";

export default function KakaoChannelProvider({
  children,
}: {
  children: ReactNode;
}) {
  useEffect(() => {
    initializeKakaoChannelSdk();
  }, []);

  return (
    <>
      {children}
      <FloatingKakaoChatButton />
    </>
  );
}
"use client";

import Link from "next/link";
import { useState } from "react";
import { MessageCircle, X } from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
import { followKakaoChannel, openKakaoChat } from "@/services/kakao-channel";

const noticeMessage =
  process.env.NEXT_PUBLIC_KAKAO_NOTICE_MESSAGE ||
  "평일 오전 10시부터 오후 7시까지 순차적으로 답변드립니다.";

export default function FloatingKakaoChatButton() {
  const [isOpen, setIsOpen] = useState(false);

  const handleOpenChat = () => {
    const followPromise = followKakaoChannel();

    if (followPromise && typeof followPromise.then === "function") {
      followPromise.catch(() => undefined).finally(() => {
        openKakaoChat();
      });
      return;
    }

    openKakaoChat();
  };

  return (
    <div className="fixed bottom-6 right-6 z-[60] flex flex-col items-end gap-3">
      <AnimatePresence>
        {isOpen ? (
          <motion.div className="flex min-h-[360px] w-80 flex-col rounded-[32px] border bg-white/95 p-5 shadow-2xl backdrop-blur dark:bg-neutral-950/95">
            <div className="rounded-3xl bg-[#FEE500] p-4 text-neutral-900">
              <p className="text-sm font-semibold">카카오톡으로 문의하기</p>
              <p className="mt-2 text-xs leading-relaxed">{noticeMessage}</p>
            </div>
            <div className="mt-6 flex flex-col gap-3">
              <button
                type="button"
                onClick={handleOpenChat}
                className="flex w-full items-center justify-center gap-2 rounded-3xl bg-[#FEE500] px-4 py-4 text-sm font-semibold text-neutral-900"
              >
                <MessageCircle className="h-4 w-4" />
                카카오톡 채팅 열기
              </button>
              <Link href="/contact" className="text-center text-xs underline underline-offset-4">
                다른 문의 방법 보기
              </Link>
            </div>
          </motion.div>
        ) : null}
      </AnimatePresence>

      <motion.button
        type="button"
        onClick={() => setIsOpen((prev) => !prev)}
        className="flex h-14 w-14 items-center justify-center rounded-full bg-[#FEE500] text-neutral-900 shadow-lg"
      >
        {isOpen ? <X className="h-5 w-5" /> : <MessageCircle className="h-5 w-5" />}
      </motion.button>
    </div>
  );
}
"use client";

const KAKAO_SDK_URL = "https://developers.kakao.com/sdk/js/kakao.min.js";
const KAKAO_CHANNEL_PLUGIN_URL =
  "https://developers.kakao.com/sdk/js/kakao.channel.min.js";
const KAKAO_JAVASCRIPT_KEY = process.env.NEXT_PUBLIC_KAKAO_JS_KEY;
const KAKAO_CHANNEL_PUBLIC_ID =
  process.env.NEXT_PUBLIC_KAKAO_CHANNEL_PUBLIC_ID;

let kakaoInitPromise: Promise<void> | null = null;

function loadScript(src: string) {
  return new Promise<void>((resolve, reject) => {
    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    script.defer = true;
    script.addEventListener("load", () => resolve());
    script.addEventListener("error", (event) => reject(event));
    document.head.appendChild(script);
  });
}

async function loadKakaoSdk() {
  if (typeof window === "undefined") return Promise.resolve();
  if (window.Kakao && window.Kakao.isInitialized?.()) return Promise.resolve();
  if (kakaoInitPromise) return kakaoInitPromise;

  kakaoInitPromise = Promise.resolve()
    .then(() => loadScript(KAKAO_SDK_URL))
    .then(() => loadScript(KAKAO_CHANNEL_PLUGIN_URL))
    .then(() => {
      if (!window.Kakao?.isInitialized?.()) {
        window.Kakao?.init?.(KAKAO_JAVASCRIPT_KEY || "");
      }
    });

  return kakaoInitPromise;
}

export function initializeKakaoChannelSdk() {
  return loadKakaoSdk();
}

export function openKakaoChat() {
  loadKakaoSdk().then(() => {
    window.Kakao?.Channel?.chat?.({
      channelPublicId: KAKAO_CHANNEL_PUBLIC_ID || "",
    });
  });
}

export function followKakaoChannel() {
  return loadKakaoSdk().then(() => {
    return window.Kakao?.Channel?.followChannel?.({
      channelPublicId: KAKAO_CHANNEL_PUBLIC_ID || "",
    });
  });
}
declare global {
  interface Window {
    Kakao?: {
      init?: (key: string) => void;
      isInitialized?: () => boolean;
      Channel?: {
        chat?: (params: { channelPublicId: string }) => void;
        followChannel?: (params: {
          channelPublicId: string;
        }) => Promise<unknown> | undefined;
      };
    };
  }
}

export {};

환경변수

NEXT_PUBLIC_KAKAO_JS_KEY=your_kakao_javascript_key
NEXT_PUBLIC_KAKAO_CHANNEL_PUBLIC_ID=_xxxxxxxxx
NEXT_PUBLIC_KAKAO_NOTICE_MESSAGE=평일 오전 10시부터 오후 7시까지 순차적으로 답변드립니다.

레이아웃 연결

import KakaoChannelProvider from "@/components/kakao-channel-provider";

export default function LocaleLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <RootProvider>
      <KakaoChannelProvider>{children}</KakaoChannelProvider>
    </RootProvider>
  );
}

준비할 것

  1. Kakao Developers에서 앱을 생성합니다.
  2. JavaScript 키를 발급받습니다.
  3. 카카오톡 채널을 개설하고 공개 ID를 확인합니다.
  4. 운영 도메인을 카카오 앱 설정에 등록합니다.