Skip to content

Commit dcca3c9

Browse files
committed
Merge branch 'master' into stage
2 parents 9edf25b + 77a5d91 commit dcca3c9

File tree

18 files changed

+630
-74
lines changed

18 files changed

+630
-74
lines changed

public/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
</script>
4040
<!-- Cloudflare (turnstile) -->
4141
<script defer src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onAppReady"></script>
42-
42+
<script async src="https://cdn.fuseplatform.net/publift/tags/2/4158/fuse.js"></script>
4343

4444
<title>React App</title>
4545
</head>
4646
<body>
4747
<noscript>You need to enable JavaScript to run this app.</noscript>
48+
<div id="fuse-sidebar"></div>
4849
<div id="root"></div>
50+
4951
<!--
5052
This HTML file is a template.
5153
If you open it directly in the browser, you will see an empty page.

src/components/common/MarkdownRender.tsx

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface MarkdownRenderProps {
2828
codeTheme?: string;
2929
onConvertFinish?: (html: string) => any;
3030
editing?: boolean;
31+
shouldShowAds?: boolean;
3132
}
3233

3334
function sanitizeEventScript(htmlString: string) {
@@ -208,6 +209,7 @@ const MarkdownRender: React.FC<MarkdownRenderProps> = ({
208209
codeTheme = 'atom-one',
209210
onConvertFinish,
210211
editing,
212+
shouldShowAds = false,
211213
}) => {
212214
const [html, setHtml] = useState(
213215
ssrEnabled
@@ -232,6 +234,7 @@ const MarkdownRender: React.FC<MarkdownRenderProps> = ({
232234
const [element, setElement] = useState<RenderedElement>(null);
233235
const [hasTagError, setHasTagError] = useState(false);
234236
const [delay, setDelay] = useState(25);
237+
const [htmlWithAds, setHtmlWithAds] = useState('');
235238

236239
const throttledUpdate = React.useMemo(() => {
237240
return throttle(delay, (markdown: string) => {
@@ -288,6 +291,115 @@ const MarkdownRender: React.FC<MarkdownRenderProps> = ({
288291
throttledUpdate(markdown);
289292
}, [markdown, throttledUpdate]);
290293

294+
useEffect(() => {
295+
if (!shouldShowAds || !html) {
296+
setHtmlWithAds('');
297+
return;
298+
}
299+
300+
const parser = new DOMParser();
301+
const doc = parser.parseFromString(html, 'text/html');
302+
const blockElements = doc.querySelectorAll(
303+
'p, h1, h2, h3, h4, h5, h6, blockquote, pre, ul, ol, hr, table',
304+
);
305+
306+
const blockCount = blockElements.length;
307+
308+
// Find the position to insert first ad (10~25 범위)
309+
let firstAdPosition = 9; // 10th block (0-based index)
310+
311+
// Look for first h1, h2, h3 from 10th to 25th block
312+
for (let i = 10; i <= 25 && i < blockElements.length; i++) {
313+
const block = blockElements[i];
314+
if (
315+
block.tagName === 'H1' ||
316+
block.tagName === 'H2' ||
317+
block.tagName === 'H3'
318+
) {
319+
firstAdPosition = i;
320+
break;
321+
}
322+
}
323+
324+
// Find the position to insert second ad (only if blockCount >= 40)
325+
let secondAdPosition = -1;
326+
if (blockCount >= 40) {
327+
// 블록이 40 이상인 경우: 35번째부터 50번째(또는 끝)까지 h1, h2, h3 찾기
328+
secondAdPosition = 34; // 35th block (0-based index)
329+
330+
const searchEnd = Math.min(50, blockElements.length);
331+
// Look for first h1, h2, h3 from 35th to searchEnd
332+
for (let i = 35; i < searchEnd; i++) {
333+
const block = blockElements[i];
334+
if (
335+
block.tagName === 'H1' ||
336+
block.tagName === 'H2' ||
337+
block.tagName === 'H3'
338+
) {
339+
secondAdPosition = i;
340+
break;
341+
}
342+
}
343+
}
344+
345+
// Insert ads
346+
const firstAdBlock = blockElements[firstAdPosition];
347+
const secondAdBlock =
348+
secondAdPosition >= 0 ? blockElements[secondAdPosition] : null;
349+
350+
if (firstAdBlock) {
351+
// Insert first ad
352+
// const adDiv1 = doc.createElement('ins');
353+
// adDiv1.className = 'adsbygoogle';
354+
// adDiv1.style.display = 'block';
355+
// adDiv1.style.textAlign = 'center';
356+
// adDiv1.setAttribute('data-ad-layout', 'in-article');
357+
// adDiv1.setAttribute('data-ad-format', 'fluid');
358+
// adDiv1.setAttribute('data-ad-client', 'ca-pub-5574866530496701');
359+
// adDiv1.setAttribute('data-ad-slot', '9632367492');
360+
const adDiv1 = doc.createElement('div');
361+
adDiv1.setAttribute('data-fuse', 'incontent_1_articlepage');
362+
firstAdBlock.parentNode?.insertBefore(adDiv1, firstAdBlock);
363+
364+
// Insert second ad if applicable
365+
if (secondAdBlock) {
366+
// const adDiv2 = doc.createElement('ins');
367+
// adDiv2.className = 'adsbygoogle';
368+
// adDiv2.style.display = 'block';
369+
// adDiv2.style.textAlign = 'center';
370+
// adDiv2.setAttribute('data-ad-layout', 'in-article');
371+
// adDiv2.setAttribute('data-ad-format', 'fluid');
372+
// adDiv2.setAttribute('data-ad-client', 'ca-pub-5574866530496701');
373+
// adDiv2.setAttribute('data-ad-slot', '9632367492');
374+
const adDiv2 = doc.createElement('div');
375+
adDiv2.setAttribute('data-fuse', 'incontent_2_articlepage');
376+
secondAdBlock.parentNode?.insertBefore(adDiv2, secondAdBlock);
377+
}
378+
379+
// Set the modified HTML
380+
const updatedHtml = doc.body.innerHTML;
381+
setHtmlWithAds(updatedHtml);
382+
383+
// Push ads after 1 second
384+
setTimeout(() => {
385+
// (window.adsbygoogle = window.adsbygoogle || []).push({});
386+
// if (secondAdBlock) {
387+
// (window.adsbygoogle = window.adsbygoogle || []).push({});
388+
// }
389+
390+
const fusetag = window.fusetag || (window.fusetag = { que: [] });
391+
392+
fusetag.que.push(function () {
393+
const init = (fusetag as any).pageInit;
394+
if (!init) return;
395+
init({});
396+
});
397+
}, 1000);
398+
} else {
399+
setHtmlWithAds('');
400+
}
401+
}, [html, shouldShowAds]);
402+
291403
return (
292404
<Typography>
293405
<Helmet>
@@ -312,7 +424,7 @@ const MarkdownRender: React.FC<MarkdownRenderProps> = ({
312424
) : (
313425
<MarkdownRenderBlock
314426
className={codeTheme}
315-
dangerouslySetInnerHTML={{ __html: html }}
427+
dangerouslySetInnerHTML={{ __html: htmlWithAds || html }}
316428
/>
317429
)}
318430
</Typography>

src/components/common/NarrowAd.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { useEffect, useState } from 'react';
2+
import styled from 'styled-components';
3+
import media from '../../lib/styles/media';
4+
5+
const Wrapper = styled.div`
6+
display: flex;
7+
justify-content: center;
8+
align-items: center;
9+
margin: 16px 0;
10+
`;
11+
12+
const AdBlock = styled.div<{ width: number; height: number }>`
13+
width: ${(props) => props.width}px;
14+
height: ${(props) => props.height}px;
15+
display: flex;
16+
justify-content: center;
17+
align-items: center;
18+
`;
19+
20+
function AdInsComponent({
21+
width,
22+
height,
23+
slot,
24+
}: {
25+
width: number;
26+
height: number;
27+
slot: string;
28+
}) {
29+
useEffect(() => {
30+
(window.adsbygoogle = window.adsbygoogle || []).push({});
31+
}, []);
32+
33+
return (
34+
<ins
35+
className="adsbygoogle"
36+
style={{
37+
display: 'inline-block',
38+
width: `${width}px`,
39+
height: `${height}px`,
40+
}}
41+
data-ad-client="ca-pub-5574866530496701"
42+
data-ad-slot={slot}
43+
></ins>
44+
);
45+
}
46+
47+
export default function NarrowAd() {
48+
const [isWindowNarrow, setIsWindowNarrow] = useState(false);
49+
const [mode, setMode] = useState<'wide' | 'narrow'>('wide');
50+
51+
useEffect(() => {
52+
const windowWidth = window.innerWidth;
53+
54+
setIsWindowNarrow(windowWidth <= 1200);
55+
56+
// Set mode based on window width
57+
setMode(windowWidth < 768 ? 'narrow' : 'wide');
58+
}, []);
59+
60+
useEffect(() => {
61+
const handleResize = () => {
62+
const windowWidth = window.innerWidth;
63+
64+
// Update isWindowNarrow based on width
65+
setIsWindowNarrow(windowWidth <= 1200);
66+
67+
// Update mode based on width
68+
setMode(windowWidth < 768 ? 'narrow' : 'wide');
69+
};
70+
71+
window.addEventListener('resize', handleResize);
72+
return () => {
73+
window.removeEventListener('resize', handleResize);
74+
};
75+
}, []);
76+
77+
const size =
78+
mode === 'narrow' ? { width: 300, height: 50 } : { width: 728, height: 90 };
79+
const adSlot = mode === 'narrow' ? '1778476944' : '5606751754';
80+
81+
return (
82+
<Wrapper>
83+
{isWindowNarrow && mode === 'narrow' && (
84+
<AdBlock width={size.width} height={size.height}>
85+
<AdInsComponent
86+
width={size.width}
87+
height={size.height}
88+
slot={adSlot}
89+
/>
90+
</AdBlock>
91+
)}
92+
{isWindowNarrow && mode === 'wide' && (
93+
<AdBlock width={size.width} height={size.height}>
94+
<AdInsComponent
95+
width={size.width}
96+
height={size.height}
97+
slot={adSlot}
98+
/>
99+
</AdBlock>
100+
)}
101+
</Wrapper>
102+
);
103+
}

src/components/common/PostCardGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const Block = styled.div`
8888
display: flex;
8989
margin: -1rem;
9090
flex-wrap: wrap;
91+
justify-content: center;
9192
${mediaQuery(767)} {
9293
margin: 0;
9394
}

src/components/common/Sticky.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const Sticky: React.FC<StickyProps> = ({ className, top, children }) => {
2222

2323
const onScroll = useCallback(() => {
2424
const scrollTop = getScrollTop();
25-
const nextFixed = scrollTop + 112 > y;
25+
const nextFixed = scrollTop + 80 > y;
2626
if (fixed !== nextFixed) {
2727
setFixed(nextFixed);
2828
}

src/components/post/FuseSideAd.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, { useEffect, useState } from 'react';
2+
import styled from 'styled-components';
3+
import { getScrollTop } from '../../lib/utils';
4+
import { createPortal } from 'react-dom';
5+
6+
const INITIAL_TOP = 510;
7+
const FIXED_TOP = 80;
8+
const MIN_HEIGHT = 694;
9+
const MIN_WIDTH = 1140;
10+
11+
const Box = styled.div<{ fixed: boolean }>`
12+
position: ${({ fixed }) => (fixed ? 'fixed' : 'absolute')};
13+
top: ${({ fixed }) => (fixed ? `${FIXED_TOP}px` : `${INITIAL_TOP}px`)};
14+
right: calc(50% + 432px);
15+
`;
16+
17+
const Actual = styled.div`
18+
width: 120px;
19+
height: 600px;
20+
21+
@media (min-width: 1219px) {
22+
width: 160px;
23+
}
24+
25+
@media (min-width: 1619px) {
26+
width: 300px;
27+
}
28+
`;
29+
30+
function FuseSideAdInner() {
31+
const [fixed, setFixed] = useState(false);
32+
33+
useEffect(() => {
34+
const onScroll = () => {
35+
const scrollTop = getScrollTop();
36+
const shouldBeFixed = scrollTop >= INITIAL_TOP - FIXED_TOP;
37+
setFixed(shouldBeFixed);
38+
};
39+
40+
window.addEventListener('scroll', onScroll);
41+
onScroll();
42+
43+
return () => {
44+
window.removeEventListener('scroll', onScroll);
45+
};
46+
}, []);
47+
48+
useEffect(() => {
49+
const fusetag = window.fusetag || (window.fusetag = { que: [] });
50+
51+
fusetag.que.push(function () {
52+
const init = (fusetag as any).pageInit;
53+
if (!init) return;
54+
init({});
55+
});
56+
}, []);
57+
58+
return (
59+
<Box fixed={fixed}>
60+
<Actual>
61+
<div data-fuse="sidebar_LHS"></div>
62+
</Actual>
63+
</Box>
64+
);
65+
}
66+
67+
export default function FuseSideAd() {
68+
const [visible, setVisible] = useState(false);
69+
70+
useEffect(() => {
71+
const checkSize = () => {
72+
const meetsHeight = window.innerHeight >= MIN_HEIGHT;
73+
const meetsWidth = window.innerWidth >= MIN_WIDTH;
74+
setVisible(meetsHeight && meetsWidth);
75+
};
76+
77+
checkSize();
78+
window.addEventListener('resize', checkSize);
79+
80+
return () => {
81+
window.removeEventListener('resize', checkSize);
82+
};
83+
}, []);
84+
85+
if (!visible) return null;
86+
87+
const target = document.getElementById('fuse-sidebar');
88+
if (!target) return null;
89+
90+
return createPortal(<FuseSideAdInner />, target);
91+
}

0 commit comments

Comments
 (0)