테스트 페이지
Jan 24, 2025
1. 서버 사이드에서 로깅하는 방법!!
- 페이지 방문 후 서버에 요청이 들어오는 시점에 blog_id, post_id 정보를 담아 빈 log 생성 후 페이지 컴포넌트 return. page 의 prop 으로 blog, post 데이터와 함께 생성한 log_id 값도 같이 전달
...
const { traffic_type } = props.searchParams;
const isInternal = traffic_type === "internal";
let logId = null;
if (!isInternal) {
const { data, error: logError } = await supabase
.from("logs")
.insert({
blog_id: postProps.blog.id,
post_id: postProps.post.id,
})
.select("id")
.single();
logId = data?.id ?? null;
}
const serverContent = (
<div
className={clsx(
"px-5 w-full",
postProps.post.content_type === "tiptap" && "tiptap"
)}
dangerouslySetInnerHTML={{ __html: contentHtml ?? "" }}
/>
);
return (
<>
{isTeamPlan && postProps.post.post_custom_scripts?.json_ld_script && (
<script
id="post-json-ld"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: postProps.post.post_custom_scripts?.json_ld_script ?? "",
}}
/>
)}
{/* body start scripts */}
{isTeamPlan && postProps.post.post_custom_scripts?.body_start_script && (
<>
{parseScripts(postProps.post.post_custom_scripts.body_start_script)}
</>
)}
<PostPage {...postProps} logId={logId} children={serverContent} />
{/* body end scripts */}
{isTeamPlan && postProps.post.post_custom_scripts?.body_end_script && (
)}
</>
);
...
- client side component 가 mount 시점에서 넘겨 받은 log_id 를 통해 client 정보를 update.
...
useEffect(() => {
if (!logId) return;
const sessionId = getOrCreateSessionId();
let userId = getCookie("_inblog_user");
if (!userId) {
userId = generateUserId();
setCookie("_inblog_user", userId);
}
const updateLog = async () => {
const { data, error } = await supabase
.from("logs")
.update({
device: isMobile()
? "mobile"
: isMobileTablet()
? "tablet"
: "desktop",
referrer: window.document.referrer
? new URL(window.document.referrer).hostname
: "direct",
full_referrer: window.document.referrer,
is_routing_back: false,
session_id: sessionId,
user_id: userId,
})
.eq("id", logId);
};
updateLog();
}, [logId]);
...
- 버튼 클릭시 마찬가지로 넘겨받은 log_id 를 통해 해당 log 의 is_click 을 true 로 업데이트
const { data, error } = await supabase
.from("logs")
.update({ is_click: true })
.eq("id", logId);
- 서버에 요청이 들어올 때 마다 어떤 블로그 어떤 포스트에 요청이 들어왔는지 먼저 기록하고, 기록한 값을 페이지에 서명해서 클라이언트에 전달해주는 느낌
- 서버에 해당 페이지에 url 로 들어오는 요청을 큰 변수 없이 catch 해서 기록 할 수 있음
- logId 값을 서버에서 전달한 상태로 page 를 내려주기 때문에, client-side 렌더링 방식과 달리 뒤로가기 등을 통해 다시 페이지로 돌아가더라도 해당 log 는 특정해서 기록할 수 있음
- 뒤로가기 케이스는 한 번 렌더링 된 페이지에 대해서 브라우저가 서버에 다시 url를 호출하지 않기 때문에 자동으로 기록되지 않음
중요! 서버 사이드 로깅을 채택하지 못한 이유
Next js <Link/> 를 사용하기 때문에, 실제로 방문하지 않은 페이지에 대하여 Next js 가 preload 하는 과정에서 log 가 serverside 에서 기록됨

방문한 위의 post_id 39981 페이지의 경우
- 서버에서 post_id 39981 로그 생성 (클라이언트 정보는 빈 상태)
- client side 에서 39981 포스트 컴포넌트의 mount 와 동시에 session_id, user_id 등 클라이언트 정보 로그에 업데이트
- hydration 이후 view port 내에 있는 author (inblog 팀 프로필), more-articles 의 포스트들 (오른쪽 2개) 에 대해 Next js Link 가 미리 정보를 호출
- 서버 사이드에서는 받은 콜에 의해 방문하지도 않은 3 페이지에 대해 빈로그 생성

session_id, user_id 가 빈 로그는 실제로 방문하지 않은 페이지라고 판단할 수는 있으나, Link 특성상 불필요한 빈 로그가 너무 많이 생성될 수 있음.
⇒ 배포 1주일 앞둔 시점에서 서버 사이드 로깅 채택하기 어려운 가장 큰 사유
2. 클라이언트 사이드에서 로깅하는 방법
- layout.tsx 상단에 <Script/> logging.js 스크립트 추가 (/public/logging.js)
- window.addEventListener “popstate” 를 통해 뒤로가기 event 를 구분
- post, blog 페이지의 post_id, blog_id 값이 서버에서 받아진 시점을 “inblog-log-event” listen 하고 “popstate” 상태와 결합하여 로깅할지 여부를 결정
- “/api/log-click” , "/api/log-view" api 를 호출하여 supabase 로깅 로직 실행
- 플로우
- routing 시 isLogging, isRoutingBack(뒤로가기 여부) 초기화
- "inblog-log-event" 이벤트를 listen 하여 post, blog 페이지가 서버에서 받은 post_id, blog_id 값을 event detail로 받음
- "inblog-log-event" 이벤트 catch 이후 50ms 버퍼 타임을 두고, “popstate” 이벤트가 정상적으로 받아졌음을 가정.
- “popstate” 여부와 event로부터 받은 post, blog id 값, 추가적인 client,user,session 정보를 /api/log-view" api 를 호출하여 supabase 로깅 로직 실행
- 같은 페이지 상에서 유저가 cta 클릭시, is_click = true 인 "inblog-log-event" 이벤트 catch, “/api/log-click” 을 통해 클릭 이벤트 기록
(function () {
const LOG_EVENT_NAME = "inblog-log-event";
let currentPath = null;
let isRoutingBack = false;
let isLogging = false;
let isInitialized = false;
window.addEventListener("inblog-log-event", async (e) => {
if (!isInitialized) return;
const searchParams = new URLSearchParams(window.location.search);
const trafficType = searchParams.get("traffic_type");
if (trafficType === "internal") return;
if (isLogging) return;
isLogging = true;
const isForClick = e.detail.is_click;
if (isForClick) {
fetch("/api/log-click", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
session_id: e.detail.session_id,
post_id: e.detail.post_id,
}),
});
} else {
// Add a 50ms delay
await new Promise((resolve) => setTimeout(resolve, 50));
// Fetch the log view API
fetch("/api/log-view", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...e.detail,
is_routing_back: isRoutingBack,
}),
});
}
isLogging = false;
});
// Listen for browser back/forward button usage
window.addEventListener("popstate", () => {
isRoutingBack = true;
});
// Handle route changes (pushState / replaceState)
function handleRouteChange() {
const newPath = window.location.pathname;
// Store the updated current path
currentPath = newPath;
isLogging = false;
isRoutingBack = false;
logId = null;
}
// Wrap native history methods to detect route changes
function rewriteHistoryMethod(methodName) {
const original = history[methodName];
return function (...args) {
original.apply(this, args);
handleRouteChange();
};
}
history.pushState = rewriteHistoryMethod("pushState");
history.replaceState = rewriteHistoryMethod("replaceState");
// Run once on initial load
handleRouteChange();
window.dispatchEvent(new Event("inblog-initial-log-event"));
isInitialized = true;
})();
스크립트의 역할
- 클라이언트 컴포넌트에 사용되는 useLogView hook
logIdRef
와 “inblog-initial-log-event”
listener 가 있는 이유
<Script src={`${INBLOG_URL}/logging.js`} strategy="afterInteractive" />
layout.tsx 의 Script code 가
strategy="afterInteractive"
(beforeInteractive
는 hydration 에러 발생) 인데, 가장 첫 페이지 렌딩 시 client component 에서 dispatch 하는
window.dispatchEvent( new CustomEvent("inblog-log-event",
시점에 script 의 listener 가 init 되지 않음. 스크립트가 실행되고 1회만
window.dispatchEvent(new Event("inblog-initial-log-event"));
하여 client component 에서 로깅하도록 처리
import { useEffect, useRef, useState } from "react";
import supabase from "@/lib/supabase";
import { useInterval } from "react-use";
import { v4 as uuidv4 } from "uuid";
export type LogType = "home" | "post" | "author" | "category";
export const LOGGING_EVENT_NAME = "inblog-log-event";
export function useLogView({
blogId,
postId,
authorId,
tagId,
logType,
}: {
blogId: number;
postId?: number;
authorId?: string;
tagId?: number;
logType: LogType;
}) {
const [isLogging, setIsLogging] = useState(false);
const logIdRef = useRef<number | null>(null);
const logPostCTAClickEvent = async () => {
if (logType !== "post" || !postId) return;
const userId = getCookie("_inblog_user");
const sessionId = sessionStorage.getItem("inblog-session-id");
if (!userId || !sessionId) return;
if (logIdRef.current) {
const { data, error } = await supabase
.from("logs")
.update({ is_click: true })
.eq("id", logIdRef.current);
if (error) console.error("Error logging post CTA click:", error);
} else {
window.dispatchEvent(
new CustomEvent("inblog-log-event", {
detail: {
is_click: true,
session_id: sessionId,
post_id: postId,
},
})
);
}
};
useEffect(() => {
const handleLogEvent = async () => {
if (
!TEST_BLOG_IDS.includes(blogId) &&
process.env.NEXT_PUBLIC_VERCEL_ENV !== "production"
) {
console.log("Skipping logging because we're not in production");
return;
}
if (typeof window === "undefined") return;
if (isLogging) return;
setIsLogging(true);
const sessionId = getOrCreateSessionId();
let userId = getCookie("_inblog_user");
if (!userId) {
userId = generateUserId();
setCookie("_inblog_user", userId);
}
// Insert log into Supabase
const { data, error } = await supabase
.from("logs")
.insert({
blog_id: blogId,
post_id: postId ?? null,
session_id: sessionId,
user_id: userId,
created_at: new Date().toISOString(),
is_click: false,
device: isMobile()
? "mobile"
: isMobileTablet()
? "tablet"
: "desktop",
referrer: window.document.referrer
? new URL(window.document.referrer).hostname
: "direct",
full_referrer: window.document.referrer,
log_type: logType,
author_uuid: authorId ?? null,
tag_id: tagId ?? null,
is_routing_back: false,
})
.select()
.single();
if (error) {
console.error("Error inserting log:", error);
return;
}
logIdRef.current = data.id;
};
// Add event listener
window.addEventListener("inblog-initial-log-event", handleLogEvent);
return () => {
window.removeEventListener("inblog-initial-log-event", handleLogEvent);
};
}, []);
useEffect(() => {
if (typeof window === "undefined") return;
if (isLogging) return;
console.log("dispatching event");
setIsLogging(true);
let userId = getCookie("_inblog_user");
if (!userId) {
userId = generateUserId();
setCookie("_inblog_user", userId);
}
window.dispatchEvent(
new CustomEvent("inblog-log-event", {
detail: {
blogId,
postId,
sessionId: getOrCreateSessionId(),
userId,
created_at: new Date().toISOString(),
is_click: false,
device: isMobile()
? "mobile"
: isMobileTablet()
? "tablet"
: "desktop",
referrer: window.document.referrer
? new URL(window.document.referrer).hostname
: "direct",
full_referrer: window.document.referrer,
log_type: logType,
author_uuid: authorId ?? null,
tag_id: tagId ?? null,
},
})
);
}, [postId, blogId]);
return {
logPostCTAClickEvent,
};
}
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
function setCookie(name: string, value: string, days = 365) {
// Default 365 days
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `; expires=${date.toUTCString()}`;
document.cookie = `${name}=${encodeURIComponent(value)}${expires}; path=/`;
}
// -- Generate a basic random user ID, e.g. for first-time visitors --
function generateUserId() {
return `_inblog_user_${Math.random().toString(36).substring(2)}`;
}
const isMobile = () => {
let check = false;
(function (a) {
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
a
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
a.substr(0, 4)
)
)
check = true;
})(window.navigator.userAgent);
return check;
};
const isMobileTablet = () => {
let check = false;
(function (a) {
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
a
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
a.substr(0, 4)
)
)
check = true;
})(window.navigator.userAgent);
return check;
};
function generateSessionId(): string {
return uuidv4();
}
function getOrCreateSessionId() {
let sessionId = sessionStorage.getItem("inblog-session-id");
if (!sessionId) {
sessionId = generateSessionId();
sessionStorage.setItem("inblog-session-id", sessionId);
}
return sessionId;
}
- 뒤로 가기 기록과 버튼 클릭 처리 방법
- 일반적인 정상 routing 과 뒤로가기/앞으로가기 버튼 routing 모두 log 에 기록
- 뒤로가기/앞으로가기 routing 은
is_routing_back
은 true 로 구분 (page view 기반 pricing 용 count 에서 제외) - A→B 이동 후, 뒤로가기 버튼으로 A 페이지로 돌아간 경우
- post_id: a, is_routing_back: false, is_click: false
- post_id: b, is_routing_back: false, is_click: false
- post_id: a, is_routing_back: true, is_click: false
- CTA 버튼 클릭 처리 방법
- CTA 클릭 시 event 로 post_id 와 session_id 전달
- session_id 와 post_id 기반으로 현재 버튼을 클릭한 log 의 row 구함
- 해당 log 의 is_routing_back 이 false 인 경우, 정상적인 방문이므로 is_click 을 true 로 update
- 해당 log 의 is_routing_back 이 true 인 경우, 뒤로가기 방문에서 버튼을 누르는 경우로 페이지 뷰 수 대비 중복 클릭이 발생할 수 잇음
- 예시 케이스: 홈 → 39981 포스트 → 39773 포스트 → 뒤로가기 버튼 클릭으로 39981 포스트 복귀 → CTA 클릭
- 홈 화면 방문 로그 생성 (4546145)
- 39981 포스트 방문 로그 생성 (4546151),
is_click
false /is_routing_back
false - 39773 포스트 방문 로그 생성 (4546152)
- 39981 포스트 방문 로그 생성 (4546157),
is_click
false /is_routing_back
true - CTA 버튼 클릭시, 로그의 투명성을 최대화 하기 위해 우선 뒤로가기로 생성된 4546157 로그의
is_click
true 로 수정 - 같은 세션의 같은 post_id 방문한 log 중
is_click
false /is_routing_back
false 인 가장 최신 로그를 찾고, - CTR 계산을 위해 click 이벤트의 수가
is_routing_back
이 true 가 아닌 (정상적인 페이지 방문) 페이지 뷰 수를 넘게 할 수 없음 - 뒤로가기를 통해 click 을 하여 이벤트가 무분별하게 기록될 시 ctr 100% 를 넘어갈 수 있기에, 별도의 처리가 필요하나 함부로 생략/가공 시 black box 이슈가 생김으로 로그를 최대한 있는 그대로 남기면서 ctr 100% 넘지 않는 처리가 필요함
- 따라서 뒤로 가기 클릭을 통해 click 한 경우 우선은 해당 뒤로가기 방문 로그에 is_click 을 true 로 하여 기록은 남김
- 뒤로가기를 통해 버튼을 클릭했다면, 이는 원래 이전에 정상 방문한 페이지 중 하나에서 클릭을 했다는 의미
- 따라서 정상 방문한 페이지 (
is_routing_back
이 true 가 아닌) 중 CTA 를 클릭하지 않은 (is_click
이 true 가 아닌) 로그를 찾아서 그 페이지에서 클릭을 했을 것이다로 처리. - 대신 기록을 정확하게 남기기 위해
click_by_back
에 뒤로가기를 통해 방문한 로그의 id를 기록하여 향후 유기적으로 판단 가능할 수 있도록 처리 - User journey 를 보여줘야할 경우 페이지 방문과 뒤로가기/앞으로가기를 통해 방문한 케이스를 다 보여줄 수 있음
- 뒤로가기/앞으로가기 하여 버튼을 클릭했다는 세부 정보도 알려줄 수 있음
- CTR 계산 시 추가로 복잡한 처리나 가공 없이 기존 방식 그대로 is_routing_back 이 true 가 아닌 페이지뷰와 is_click 값을 찾아서 계산해줄 수 있음
DECLARE
current_log RECORD;
prev_log RECORD;
BEGIN
-- 1. Fetch the latest log row (by highest id) for the given session_id and post_id,
-- but only select the columns we need.
SELECT id, is_routing_back, is_click
INTO current_log
FROM logs
WHERE session_id = p_session_id
AND post_id = p_post_id
ORDER BY id DESC
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'No log record found for session_id: %, post_id: %',
p_session_id, p_post_id;
END IF;
-- 2. If the current_log is already clicked, return early.
IF current_log.is_click THEN
RETURN;
END IF;
-- 3. If is_routing_back is false, mark the current log as clicked and return.
IF current_log.is_routing_back = false THEN
UPDATE logs
SET is_click = true
WHERE id = current_log.id;
RETURN;
END IF;
-- 4. If is_routing_back is true:
-- (a) Mark the current log as clicked.
UPDATE logs
SET is_click = true
WHERE id = current_log.id;
-- (b) Find the most recent previous log (by id < current_log.id) where:
-- - is_routing_back = false
-- - is_click = false or is_click IS NULL
-- (and has the same session_id, post_id).
-- Only select the 'id' of that log, as that’s all we need for the update.
SELECT id
INTO prev_log
FROM logs
WHERE session_id = p_session_id
AND post_id = p_post_id
AND is_routing_back = false
AND (is_click = false OR is_click IS NULL)
AND id < current_log.id
ORDER BY id DESC
LIMIT 1;
-- 5. If no matching previous log is found, just return.
IF NOT FOUND THEN
RETURN;
END IF;
-- 6. Otherwise, mark that previous log as clicked and set click_by_back to the current log's id.
UPDATE logs
SET is_click = true,
click_by_back = current_log.id
WHERE id = prev_log.id;
END;

해당 log 의
is_click
true 로 수정, 동시에 click_by_back
의 id 값에 4546157 값을 넣어 출처를 기록⇒ 뒤로 가기 로그에서의 클릭을 기록함과 동시에, 일반적인 방문을 통한 로그의 클릭을 수정해주며 출처를 기록
위의 방법을 사용한 이유
위 방법의 장점
Share article