1- import { useState , useEffect , useRef } from ' react' ;
2- import { handleApiError } from ' @/utils/handleApiError' ;
1+ import { useState , useEffect , useRef } from " react" ;
2+ import { handleApiError } from " @/utils/handleApiError" ;
33
44export interface APIResponse {
55 title : string ;
66 description : string ;
77 image : string ;
8- siteName : string ;
9- hostname : string ;
8+ siteName ? : string ;
9+ hostname ? : string ;
1010}
1111
1212/**
13- * isValidResponse
14- * APIResponse가 유효한지를 체크합니다.
15- * (필수 필드가 빈 문자열이면 유효하지 않다고 간주)
13+ * 필수 필드(title·image)가 채워져 있는지 확인
1614 */
1715export const isValidResponse = ( res : APIResponse | null ) : boolean => {
18- if ( ! res ) return false ;
19- return (
20- res . title !== '' &&
21- // description은 빈 문자열이어도 허용할 경우 주석 처리
22- res . image !== ''
23- ) ;
24- } ;
25-
26- // 프록시 서버 URL (CORS 헤더가 추가되어야 합니다)
27- // corsproxy.io 는 요청 URL을 쿼리스트링으로 전달합니다.
28- // const proxyUrl = 'https://corsproxy.io/?key=****&url=';
29- const proxyUrl = 'https://corsproxy.io/?url=' ; // localhost용 프록시
30-
31- /**
32- * extractMetaContent
33- * 정규표현식을 사용하여 HTML에서 meta 태그의 content 값을 추출합니다.
34- * property 또는 name 속성을 모두 고려합니다.
35- */
36- const extractMetaContent = ( html : string , key : string ) : string => {
37- const regex = new RegExp (
38- `<meta[^>]*(?:property|name)=["']${ key } ["'][^>]*content=["']([^"']*)["'][^>]*>` ,
39- 'i'
40- ) ;
41- const match = html . match ( regex ) ;
42- return match ? match [ 1 ] : '' ;
43- } ;
44-
45- /**
46- * cleanHTML
47- * HTML 문자열에서 주석을 제거합니다.
48- * (주석 안에 있는 OG 태그도 파싱할 수 있도록)
49- */
50- const cleanHTML = ( html : string ) : string => {
51- return html . replace ( / < ! - - [ \s \S ] * ?- - > / g, '' ) ; // 주석 제거
16+ return ! ! ( res && res . title && res . image ) ;
5217} ;
5318
19+ /* ------------------------------------------------------------------ */
20+ /* 환경별 API ORIGIN 결정 */
21+ /* ------------------------------------------------------------------ */
5422/**
55- * parseHTML
56- * HTML 문자열을 파싱하여 OG 메타 태그 정보를 추출합니다.
57- * DOMParser와 정규표현식 fallback을 함께 사용합니다.
23+ * dev : https://dev.farmsystem.kr
24+ * prod : https://farmsystem.kr
25+ * SSR : NEXT_PUBLIC_API_ORIGIN 환경변수 우선
5826 */
59- const parseHTML = ( html : string , originalUrl : string ) : APIResponse => {
60- // 먼저 주석 제거 (OG 태그가 주석에 있을 경우 보완)
61- const clean = cleanHTML ( html ) ;
62- const parser = new DOMParser ( ) ;
63- const doc = parser . parseFromString ( clean , 'text/html' ) ;
64-
65- // OG 메타 태그 추출
66- let title =
67- doc . querySelector ( 'meta[property="og:title"], meta[name="og:title"]' ) ?. getAttribute ( 'content' ) || '' ;
68- let description =
69- doc . querySelector ( 'meta[property="og:description"], meta[name="og:description"]' ) ?. getAttribute ( 'content' ) || '' ;
70- let image =
71- doc . querySelector ( 'meta[property="og:image"], meta[name="og:image"]' ) ?. getAttribute ( 'content' ) || '' ;
72- let siteName =
73- doc . querySelector ( 'meta[property="og:site_name"], meta[name="og:site_name"]' ) ?. getAttribute ( 'content' ) || '' ;
74- let ogUrl =
75- doc . querySelector ( 'meta[property="og:url"], meta[name="og:url"]' ) ?. getAttribute ( 'content' ) || originalUrl ;
76-
77- // fallback: 정규표현식으로 추출 (값이 비어있으면)
78- if ( ! title ) {
79- title = extractMetaContent ( clean , 'og:title' ) ;
80- }
81- if ( ! description ) {
82- description = extractMetaContent ( clean , 'og:description' ) ;
27+ const resolveApiOrigin = ( ) : string => {
28+ if ( typeof process !== "undefined" && process . env . NEXT_PUBLIC_API_ORIGIN ) {
29+ return process . env . NEXT_PUBLIC_API_ORIGIN ;
8330 }
84- if ( ! image ) {
85- image = extractMetaContent ( clean , 'og:image' ) ;
31+ if ( typeof window !== "undefined" ) {
32+ const host = window . location . hostname ;
33+ // localhost 나 *.local → 동일 오리진 프록시 사용
34+ if ( host === "localhost" || host . endsWith ( ".local" ) ) return "" ;
35+ // dev 스테이징 도메인
36+ if ( host . startsWith ( "dev." ) ) return "https://dev.farmsystem.kr" ;
8637 }
87- if ( ! siteName ) {
88- siteName = extractMetaContent ( clean , 'og:site_name' ) ;
89- }
90- if ( ! ogUrl ) {
91- ogUrl = originalUrl ;
92- }
93-
94- // ogUrl을 이용해 hostname 추출
95- let hostname = '' ;
96- try {
97- hostname = new URL ( ogUrl ) . hostname ;
98- } catch {
99- hostname = '' ; // URL 파싱 실패 시 빈 문자열
100- }
101-
102- return { title, description, image, siteName, hostname } ;
38+ // 기본: 프로덕션 도메인
39+ return "https://farmsystem.kr" ;
10340} ;
10441
42+ /* ------------------------------------------------------------------ */
43+ /* useLinkPreview – Serverless JSON 응답 전용 */
44+ /* ------------------------------------------------------------------ */
10545export const useLinkPreview = (
10646 url : string ,
107- fetcher ?: ( url : string ) => Promise < APIResponse | null >
47+ fetcher ?: ( endpoint : string ) => Promise < APIResponse | null >
10848) => {
10949 const [ metadata , setMetadata ] = useState < APIResponse | null > ( null ) ;
11050 const [ loading , setLoading ] = useState < boolean > ( true ) ;
111- const isMounted = useRef ( true ) ;
11251 const [ error , setError ] = useState < Error | null > ( null ) ;
52+ const isMounted = useRef ( true ) ;
11353
11454 useEffect ( ( ) => {
55+ if ( ! url ) {
56+ setMetadata ( null ) ;
57+ setLoading ( false ) ;
58+ return ;
59+ }
60+
11561 isMounted . current = true ;
11662 setLoading ( true ) ;
11763
118- // 프록시를 통한 요청 URL 구성
119- const proxyFetchUrl = proxyUrl + encodeURIComponent ( url ) ;
64+ const apiOrigin = resolveApiOrigin ( ) ;
65+ const base = apiOrigin ? `${ apiOrigin } /api/og?url=` : "/api/og?url=" ; // 로컬(dev 서버)일 때 동일 오리진 프록시 사용
66+ const endpoint = `${ base } ${ encodeURIComponent ( url ) } ` ;
12067
121- if ( fetcher ) {
122- fetcher ( proxyFetchUrl )
123- . then ( ( res ) => {
124- if ( isMounted . current ) {
125- if ( isValidResponse ( res ) ) {
126- setMetadata ( res ) ;
127- } else {
128- setMetadata ( null ) ; // 유효하지 않으면 null 처리
129- }
130- setLoading ( false ) ;
131- }
132- } )
133- . catch ( ( err ) => {
134- setError ( handleApiError ( err ) ) ;
135- if ( isMounted . current ) {
136- setMetadata ( null ) ;
137- setLoading ( false ) ;
138- }
139- } ) ;
140- } else {
141- // 기본 fetcher: 프록시 서버를 통해 HTML 텍스트를 가져와 파싱
142- fetch ( proxyFetchUrl )
143- . then ( ( res ) => res . text ( ) )
144- . then ( ( html ) => {
145- if ( isMounted . current ) {
146- try {
147- const parsedData = parseHTML ( html , url ) ;
148- setMetadata ( parsedData ) ; // OG 메타 태그 파싱한 결과를 상태로 저장
149- } catch ( parseError ) {
150- setError ( handleApiError ( parseError ) ) ;
151- setMetadata ( null ) ;
152- }
153- setLoading ( false ) ;
154- }
155- } )
156- . catch ( ( err ) => {
157- setError ( handleApiError ( err ) ) ;
158- if ( isMounted . current ) {
159- setMetadata ( null ) ;
160- setLoading ( false ) ;
161- }
162- } ) ;
163- }
68+ const doFetch = async ( ) => {
69+ try {
70+ const res = fetcher ? await fetcher ( endpoint ) : await defaultFetcher ( endpoint ) ;
71+ if ( ! isMounted . current ) return ;
72+ setMetadata ( isValidResponse ( res ) ? res : null ) ;
73+ } catch ( err ) {
74+ if ( ! isMounted . current ) return ;
75+ setError ( handleApiError ( err ) ) ;
76+ setMetadata ( null ) ;
77+ } finally {
78+ if ( isMounted . current ) setLoading ( false ) ;
79+ }
80+ } ;
81+
82+ doFetch ( ) ;
16483
16584 return ( ) => {
16685 isMounted . current = false ;
@@ -169,3 +88,20 @@ export const useLinkPreview = (
16988
17089 return { metadata, loading, error } ;
17190} ;
91+
92+ /* ------------------------------------------------------------------ */
93+ /* 기본 fetcher – serverless JSON */
94+ /* ------------------------------------------------------------------ */
95+ const defaultFetcher = async ( endpoint : string ) : Promise < APIResponse | null > => {
96+ const res = await fetch ( endpoint , { credentials : "omit" } ) ;
97+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
98+ const data : APIResponse = await res . json ( ) ;
99+
100+ // hostname 보완(없을 경우)
101+ if ( ! data . hostname ) {
102+ try {
103+ data . hostname = new URL ( endpoint . split ( "url=" ) [ 1 ] ) . hostname ;
104+ } catch { /* ignore */ }
105+ }
106+ return data ;
107+ } ;
0 commit comments