@@ -13,6 +13,8 @@ import SummaryIcon from '@/components/icons/SummaryIcon';
1313import { Stomp } from '@stomp/stompjs' ;
1414import SockJS from 'sockjs-client' ;
1515import { SOCKET_CONFIG , SOCKET_CONNECTION_TYPE } from '@/constants/socketConstants' ;
16+ import SelectableText from '@/components/SelectableText' ;
17+ import HighlightedText from '@/components/HighlightedText' ;
1618
1719
1820// 이미지 URL에서 사이즈 정보 제거하는 함수
@@ -31,11 +33,27 @@ const NewsDetailPage = () => {
3133 const [ isSummaryOpen , setIsSummaryOpen ] = useState ( false ) ;
3234 const [ userCount , setUserCount ] = useState ( 0 ) ;
3335 const [ isChatOpen , setIsChatOpen ] = useState ( false ) ;
36+ const [ isChatLoading , setIsChatLoading ] = useState ( false ) ;
37+ const [ selectedQuote , setSelectedQuote ] = useState ( null ) ;
3438 const chatRoomRef = useRef ( null ) ;
3539 const [ showErrorToast , setShowErrorToast ] = useState ( false ) ;
3640 const [ errorMessage , setErrorMessage ] = useState ( '' ) ;
3741 const socketRef = useRef ( null ) ;
38-
42+ const [ highlightSegments , setHighlightSegments ] = useState ( [ ] ) ;
43+
44+ // 인용구 클릭 시 해당 문단으로 스크롤 이동하는 함수
45+ const handleQuoteScroll = ( paragraphIndex ) => {
46+ const newsContent = document . getElementById ( 'news-content' ) ;
47+ if ( newsContent ) {
48+ const paragraphs = newsContent . getElementsByTagName ( 'p' ) ;
49+ if ( paragraphs [ paragraphIndex ] ) {
50+ paragraphs [ paragraphIndex ] . scrollIntoView ( {
51+ behavior : 'smooth' ,
52+ block : 'center'
53+ } ) ;
54+ }
55+ }
56+ } ;
3957
4058 const scrollToChatRoom = ( ) => {
4159 chatRoomRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
@@ -69,6 +87,7 @@ const NewsDetailPage = () => {
6987
7088 setNews ( newsData ) ;
7189 setSelectedCategory ( getCategoryId ( data . category ) ) ;
90+ setHighlightSegments ( data . highlightSegments || [ ] ) ;
7291 } catch ( error ) {
7392 console . error ( '뉴스를 가져오는데 실패했습니다:' , error ) ;
7493 setError ( error . message ) ;
@@ -110,42 +129,76 @@ const NewsDetailPage = () => {
110129
111130 // 채팅 에러 핸들러 추가
112131 const handleChatError = ( error ) => {
113- // if (error.code === 'AUTH_REQUIRED') {
114- // setErrorMessage('로그인이 필요한 서비스입니다.');
115- // setShowErrorToast(true);
116- // setIsChatOpen(false);
117-
118- // // 3초 후 토스트 메시지 숨기기
119- // setTimeout(() => {
120- // setShowErrorToast(false);
121- // }, 3000);
122- // }
123132 setErrorMessage ( '채팅 서비스 연결에 실패했습니다.' ) ;
124- setShowErrorToast ( true ) ;
125- setIsChatOpen ( false ) ;
126-
127- // 3초 후 토스트 메시지 숨기기
128- setTimeout ( ( ) => {
129- setShowErrorToast ( false ) ;
130- } , 3000 ) ;
133+ setShowErrorToast ( true ) ;
134+ setIsChatOpen ( false ) ;
135+ setIsChatLoading ( false ) ;
136+
137+ // 3초 후 토스트 메시지 숨기기
138+ setTimeout ( ( ) => {
139+ setShowErrorToast ( false ) ;
140+ } , 3000 ) ;
131141 } ;
132142
143+ // 문단별 하이라이트 분리
144+ const getHighlightsForParagraph = ( idx ) =>
145+ highlightSegments . filter ( seg => seg . paragraphIndex === idx ) ;
146+
147+ // // 스크랩 후 하이라이트 정보 갱신
148+ // const handleSendScrap = async ({ snippetText, startOffset, endOffset, paragraphIndex }) => {
149+ // await fetch(`api/public/news/${params.id}/scrap`, {
150+ // method: 'POST',
151+ // headers: { 'Content-Type': 'application/json' },
152+ // body: JSON.stringify({ snippetText, startOffset, endOffset, paragraphIndex })
153+ // });
154+ // // 스크랩 저장 후, 다시 하이라이트 정보 갱신
155+ // const res2 = await getNewsDetail(params.id);
156+ // setHighlightSegments(res2.data.highlightSegments || []);
157+ // };
158+
133159 // content를 문단별로 나누는 함수
134160 const parseContent = ( content ) => {
161+ // 일반 텍스트 렌더링 함수
162+ const renderText = ( text , key = 0 ) => (
163+ < p key = { key } className = "mb-4 leading-relaxed text-lg" >
164+ < HighlightedText
165+ text = { text }
166+ highlights = { getHighlightsForParagraph ( key ) }
167+ />
168+ </ p >
169+ ) ;
170+
171+ // SelectableText로 감싸서 렌더링하는 함수
172+ const renderSelectableText = ( text , index = 0 ) => (
173+ < p key = { index } className = "mb-4 leading-relaxed text-lg" >
174+ < SelectableText
175+ text = { text }
176+ paragraphIndex = { index }
177+ onSend = { ( selectionInfo ) => {
178+ console . log ( 'Selected text info:' , selectionInfo ) ;
179+ setSelectedQuote ( selectionInfo ) ;
180+ } }
181+ >
182+ < HighlightedText
183+ text = { text }
184+ highlights = { getHighlightsForParagraph ( index ) }
185+ />
186+ </ SelectableText >
187+ </ p >
188+ ) ;
189+
135190 try {
136191 // 문자열을 파싱하여 배열로 변환
137192 const paragraphs = JSON . parse ( content ) ;
138193
139- // 각 문단을 p 태그로 감싸서 반환
140- return paragraphs . map ( ( paragraph , index ) => (
141- < p key = { index } className = "mb-4 leading-relaxed text-lg" >
142- { parse ( paragraph ) }
143- </ p >
144- ) ) ;
194+ // 각 문단을 렌더링
195+ return paragraphs . map ( ( paragraph , index ) =>
196+ isChatOpen ? renderSelectableText ( paragraph , index ) : renderText ( paragraph , index )
197+ ) ;
145198 } catch ( error ) {
146199 console . error ( 'Content 파싱 중 오류 발생:' , error ) ;
147- // 파싱 실패 시 원본 content를 그대로 반환
148- return < p className = "mb-4 leading-relaxed text-lg" > { parse ( content ) } </ p > ;
200+ // 문자열 파싱 실패 시 원본 content를 그대로 반환
201+ return isChatOpen ? renderSelectableText ( content ) : renderText ( content ) ;
149202 }
150203 } ;
151204
@@ -279,7 +332,7 @@ const NewsDetailPage = () => {
279332
280333 { /* 본문 */ }
281334 < div className = "space-y-6" >
282- < div className = "prose prose-lg max-w-none" >
335+ < div id = "news-content" className = "prose prose-lg max-w-none" >
283336 { parseContent ( news . content ) }
284337 </ div >
285338 </ div >
@@ -333,7 +386,18 @@ const NewsDetailPage = () => {
333386 </ div >
334387 </ div >
335388 < div className = "flex-1 overflow-y-auto" >
336- < ChatRoom articleId = { params . id } onError = { handleChatError } isPcVersion = { true } />
389+ < ChatRoom
390+ articleId = { params . id }
391+ onError = { handleChatError }
392+ isPcVersion = { true }
393+ isChatOpen = { isChatOpen }
394+ setIsChatOpen = { setIsChatOpen }
395+ selectedQuote = { selectedQuote }
396+ setSelectedQuote = { setSelectedQuote }
397+ onQuoteClick = { handleQuoteScroll }
398+ isChatLoading = { isChatLoading }
399+ setIsChatLoading = { setIsChatLoading }
400+ />
337401 </ div >
338402 </ div >
339403
@@ -363,8 +427,7 @@ const NewsDetailPage = () => {
363427 </ div >
364428
365429 { /* 뉴스 컨테이너 - 카드 스타일 */ }
366- < div className = "bg-white rounded-[2rem] shadow-inner flex flex-col h-[calc(100vh-10rem)] overflow-hidden" >
367- { /* 위와 동일한 내용 반복 */ }
430+ < div className = "bg-white rounded-[2rem] shadow-inner" >
368431 < div className = "p-6 sm:p-4" >
369432 { /* 카테고리 */ }
370433 < div className = "flex items-center gap-4 mb-4" >
@@ -413,8 +476,8 @@ const NewsDetailPage = () => {
413476 </ div >
414477 </ div >
415478
416- { /* 뉴스 본문 영역 - 스크롤 가능 */ }
417- < div className = "flex-1 overflow-y-auto px-6 sm:px-4" >
479+ { /* 뉴스 본문 영역 */ }
480+ < div className = "px-6 sm:px-4" >
418481 { /* 요약 섹션 */ }
419482 < div className = { `mb-6 transition-all duration-300 ease-in-out ${ isSummaryOpen ? 'opacity-100 max-h-[500px]' : 'opacity-0 max-h-0 overflow-hidden' } ` } >
420483 < div className = "pt-2" >
@@ -438,7 +501,7 @@ const NewsDetailPage = () => {
438501
439502 { /* 본문 */ }
440503 < div className = "space-y-6" >
441- < div className = "prose prose-lg max-w-none" >
504+ < div id = "news-content" className = "prose prose-lg max-w-none" >
442505 { parseContent ( news . content ) }
443506 </ div >
444507 </ div >
@@ -462,23 +525,32 @@ const NewsDetailPage = () => {
462525 </ div >
463526
464527 { /* 채팅방 영역 */ }
465- < div className = "bg-white shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]" ref = { chatRoomRef } >
528+ < div className = "fixed bottom-0 left-0 right-0 bg-white shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] z-50 " ref = { chatRoomRef } >
466529 < button
467530 onClick = { ( ) => setIsChatOpen ( ! isChatOpen ) }
468- className = "w-full py-3 px-4 flex items-center justify-between hover:bg-gray-50 transition-colors"
531+ disabled = { isChatLoading }
532+ className = { `w-full py-3 px-4 flex items-center justify-between transition-colors ${
533+ isChatLoading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'
534+ } `}
469535 >
470536 < span className = "text-sm text-gray-500" > 현재 { userCount } 명이 참여 중</ span >
471537 < div className = "flex items-center gap-2 text-[#0E74F9]" >
472- < span className = "font-medium" > 채팅방 { isChatOpen ? '닫기' : '열기' } </ span >
473- < svg
474- xmlns = "http://www.w3.org/2000/svg"
475- className = { `h-5 w-5 transform transition-transform ${ isChatOpen ? 'rotate-180' : '' } ` }
476- fill = "none"
477- viewBox = "0 0 24 24"
478- stroke = "currentColor"
479- >
480- < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M19 9l-7 7-7-7" />
481- </ svg >
538+ { isChatLoading ? (
539+ < span className = "font-medium" > 연결 중...</ span >
540+ ) : (
541+ < >
542+ < span className = "font-medium" > 채팅방 { isChatOpen ? '닫기' : '열기' } </ span >
543+ < svg
544+ xmlns = "http://www.w3.org/2000/svg"
545+ className = { `h-5 w-5 transform transition-transform ${ isChatOpen ? '' : 'rotate-180' } ` }
546+ fill = "none"
547+ viewBox = "0 0 24 24"
548+ stroke = "currentColor"
549+ >
550+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M19 9l-7 7-7-7" />
551+ </ svg >
552+ </ >
553+ ) }
482554 </ div >
483555 </ button >
484556 < div
@@ -487,7 +559,18 @@ const NewsDetailPage = () => {
487559 } bg-white`}
488560 >
489561 < div className = "h-full pb-safe" >
490- < ChatRoom articleId = { params . id } onError = { handleChatError } isPcVersion = { false } />
562+ < ChatRoom
563+ articleId = { params . id }
564+ onError = { handleChatError }
565+ isPcVersion = { false }
566+ isChatOpen = { isChatOpen }
567+ setIsChatOpen = { setIsChatOpen }
568+ selectedQuote = { selectedQuote }
569+ setSelectedQuote = { setSelectedQuote }
570+ onQuoteClick = { handleQuoteScroll }
571+ isChatLoading = { isChatLoading }
572+ setIsChatLoading = { setIsChatLoading }
573+ />
491574 </ div >
492575 </ div >
493576 </ div >
0 commit comments