@@ -34,34 +34,6 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ text, className, user, retry,
3434 } ) ;
3535 } ;
3636
37- // 꼬리표 색상 추출 함수
38- const getTailColorClass = ( className : string | undefined , direction : 'left' | 'right' ) => {
39- if ( ! className ) return '' ;
40- const bgClass = className . split ( ' ' ) . find ( c => c . startsWith ( 'bg-' ) ) ;
41- if ( ! bgClass ) return '' ;
42- // bg-indigo-500 -> border-l-indigo-500 또는 border-r-indigo-500
43- return direction === 'left'
44- ? bgClass . replace ( 'bg-' , 'border-r-' )
45- : bgClass . replace ( 'bg-' , 'border-l-' ) ;
46- } ;
47-
48- // 꼬리표 색상 추출 함수 (bg- 색상값을 실제 hex로 변환)
49- const getTailColorStyle = ( className : string | undefined ) => {
50- if ( ! className ) return { } ;
51- const bgClass = className . split ( ' ' ) . find ( c => c . startsWith ( 'bg-' ) ) ;
52- if ( ! bgClass ) return { } ;
53- // Tailwind의 주요 색상만 매핑 (필요시 추가)
54- const colorMap : { [ key : string ] : string } = {
55- 'bg-indigo-500' : '#4F46E5' ,
56- 'bg-blue-500' : '#3b82f6' ,
57- 'bg-gray-600' : '#4b5563' ,
58- 'bg-purple-500' : '#a21caf' ,
59- 'bg-green-500' : '#22c55e' ,
60- // 필요시 추가
61- } ;
62- return { borderLeftColor : colorMap [ bgClass ] || '#6366f1' , borderRightColor : colorMap [ bgClass ] || '#6366f1' } ;
63- } ;
64-
6537 if ( isIntroMessage ) {
6638 // 환영 메시지는 별도 레이아웃으로 분리 (배경색 제거)
6739 return (
@@ -77,11 +49,11 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ text, className, user, retry,
7749 < div
7850 ref = { messageRef }
7951 className = { `
80- relative p-3 rounded-lg
81- max-w-[70 %] max-sm:max-w-[80%]
82- break-words mb-6
83- text-[1rem] max-sm:text-[0.95rem]
84- ${ className }
52+ ${ user === "나"
53+ ? "relative p-3 rounded-lg max-w-[80 %] max-sm:max-w-[85%] break-words mb-6 text-[1rem] max-sm:text-[0.95rem]"
54+ : "relative p-0 max-w-[90%] max-sm:max-w-[95%] break-words mb-6 text-[1rem] max-sm:text-[0.95rem] bg-transparent border-none"
55+ }
56+ ${ user === "나" ? className : "" }
8557 ` }
8658 >
8759 { /* 유저 메시지면 메시지 외부 좌측에 재전송 버튼 항상 표시 */ }
@@ -94,111 +66,115 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ text, className, user, retry,
9466 ↻
9567 </ button >
9668 ) }
97- < ReactMarkdown
98- remarkPlugins = { [ remarkGfm , remarkBreaks ] }
99- rehypePlugins = { [ rehypeRaw ] }
100- components = { {
101- // 모바일에서 마크다운 요소들의 텍스트 크기를 더 작게
102- p : ( { node, ...props } ) => < p className = "text-[1.05rem] max-sm:text-[0.8rem]" { ...props } /> ,
103- h1 : ( { node, ...props } ) => < h1 className = "text-[1.5rem] max-sm:text-[1.05rem] font-bold my-2" { ...props } /> ,
104- h2 : ( { node, ...props } ) => < h2 className = "text-[1.3rem] max-sm:text-[1rem] font-bold my-2" { ...props } /> ,
105- h3 : ( { node, ...props } ) => < h3 className = "text-[1.1rem] max-sm:text-[0.95rem] font-bold my-1" { ...props } /> ,
106- h4 : ( { node, ...props } ) => < h4 className = "text-[1rem] max-sm:text-[0.9rem] font-bold my-1" { ...props } /> ,
107- ul : ( { node, ...props } ) => < ul className = "pl-5 max-sm:text-[0.8rem] list-disc my-2" { ...props } /> ,
108- ol : ( { node, ...props } ) => < ol className = "pl-5 max-sm:text-[0.8rem] list-decimal my-2" { ...props } /> ,
109- li : ( { node, ...props } ) => < li className = "my-1 max-sm:my-[0.15rem]" { ...props } /> ,
110- blockquote : ( { node, ...props } ) => < blockquote className = "border-l-4 pl-3 italic border-gray-400 max-sm:text-[0.8rem]" { ...props } /> ,
111- code : ( { node, children, className, ...props } ) => {
112- const isInline = ! ( className && className . includes ( "language-" ) ) ;
113- const codeString = String ( children ) . trim ( ) ;
114- const language = className ?. replace ( "language-" , "" ) || "javascript" ;
115- if ( isInline ) {
69+
70+ { /* AI 답변용 아바타 및 컨테이너 */ }
71+ { user !== "나" && (
72+ < div className = "flex items-start gap-3 mb-2" >
73+ < div className = "w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0" >
74+ AI
75+ </ div >
76+ < div className = "flex-1 min-w-0" >
77+ < div className = "text-gray-300 text-sm font-medium mb-1" > Assistant</ div >
78+ </ div >
79+ </ div >
80+ ) }
81+
82+ < div className = { user === "나" ? "" : "ml-11" } >
83+ < ReactMarkdown
84+ remarkPlugins = { [ remarkGfm , remarkBreaks ] }
85+ rehypePlugins = { [ rehypeRaw ] }
86+ components = { {
87+ // 모바일에서 마크다운 요소들의 텍스트 크기를 더 작게
88+ p : ( { node, ...props } ) => < p className = { `${ user === "나" ? "text-[1.05rem] max-sm:text-[0.8rem]" : "text-gray-100 text-[1rem] max-sm:text-[0.9rem] leading-relaxed mb-3" } ` } { ...props } /> ,
89+ h1 : ( { node, ...props } ) => < h1 className = { `${ user === "나" ? "text-[1.5rem] max-sm:text-[1.05rem] font-bold my-2" : "text-gray-100 text-[1.4rem] max-sm:text-[1.2rem] font-bold my-3" } ` } { ...props } /> ,
90+ h2 : ( { node, ...props } ) => < h2 className = { `${ user === "나" ? "text-[1.3rem] max-sm:text-[1rem] font-bold my-2" : "text-gray-100 text-[1.2rem] max-sm:text-[1.1rem] font-bold my-3" } ` } { ...props } /> ,
91+ h3 : ( { node, ...props } ) => < h3 className = { `${ user === "나" ? "text-[1.1rem] max-sm:text-[0.95rem] font-bold my-1" : "text-gray-100 text-[1.1rem] max-sm:text-[1rem] font-bold my-2" } ` } { ...props } /> ,
92+ h4 : ( { node, ...props } ) => < h4 className = { `${ user === "나" ? "text-[1rem] max-sm:text-[0.9rem] font-bold my-1" : "text-gray-100 text-[1rem] max-sm:text-[0.95rem] font-bold my-2" } ` } { ...props } /> ,
93+ ul : ( { node, ...props } ) => < ul className = { `pl-5 list-disc my-2 ${ user === "나" ? "max-sm:text-[0.8rem]" : "text-gray-100 max-sm:text-[0.9rem]" } ` } { ...props } /> ,
94+ ol : ( { node, ...props } ) => < ol className = { `pl-5 list-decimal my-2 ${ user === "나" ? "max-sm:text-[0.8rem]" : "text-gray-100 max-sm:text-[0.9rem]" } ` } { ...props } /> ,
95+ li : ( { node, ...props } ) => < li className = { `my-1 max-sm:my-[0.15rem] ${ user !== "나" ? "text-gray-100" : "" } ` } { ...props } /> ,
96+ blockquote : ( { node, ...props } ) => < blockquote className = { `border-l-4 pl-3 italic my-2 ${ user === "나" ? "border-gray-400 max-sm:text-[0.8rem]" : "border-blue-400 text-gray-200 max-sm:text-[0.9rem]" } ` } { ...props } /> ,
97+ code : ( { node, children, className, ...props } ) => {
98+ const isInline = ! ( className && className . includes ( "language-" ) ) ;
99+ const codeString = String ( children ) . trim ( ) ;
100+ const language = className ?. replace ( "language-" , "" ) || "javascript" ;
101+ if ( isInline ) {
102+ return (
103+ < code className = { `px-1 py-[0.5px] rounded-sm max-sm:text-[0.7rem] ${ user === "나" ? "bg-[#222]" : "bg-gray-700 text-gray-100" } ` } { ...props } >
104+ { children }
105+ </ code >
106+ ) ;
107+ }
116108 return (
117- < code className = "bg-[#222] px-1 py-[0.5px] rounded-sm max-sm:text-[0.7rem]" { ...props } >
118- { children }
119- </ code >
120- ) ;
121- }
122- return (
123- < div className = "relative my-2" >
124- < div
125- className = "rounded-lg"
126- style = { {
127- overflowX : 'auto' ,
128- width : '100%' ,
129- maxWidth : '100%' ,
130- } }
131- >
132- < SyntaxHighlighter
133- language = { language }
134- style = { atomDark }
135- customStyle = { {
136- borderRadius : 8 ,
137- fontSize : window . innerWidth <= 640 ? '0.92rem' : '0.98rem' ,
138- padding : window . innerWidth <= 640 ? '0.8em 0.7em' : '0.7em 0.8em' ,
139- margin : 0 ,
140- background : window . innerWidth <= 640 ? '#23232b' : '#18181b' ,
109+ < div className = "relative my-4" >
110+ < div
111+ className = "rounded-lg overflow-hidden"
112+ style = { {
141113 overflowX : 'auto' ,
142- minWidth : 600 , // 코드블럭이 컨테이너보다 넓게
143- width : 'fit-content' , // 코드 길이에 따라 넓이 결정
144- maxWidth : 'none' , // 최대 넓이 제한 해제
114+ width : '100%' ,
115+ maxWidth : '100%' ,
116+ } }
117+ >
118+ < SyntaxHighlighter
119+ language = { language }
120+ style = { atomDark }
121+ customStyle = { {
122+ borderRadius : 8 ,
123+ fontSize : window . innerWidth <= 640 ? '0.92rem' : '0.98rem' ,
124+ padding : window . innerWidth <= 640 ? '0.8em 0.7em' : '0.7em 0.8em' ,
125+ margin : 0 ,
126+ background : '#1a1a1a' ,
127+ overflowX : 'auto' ,
128+ minWidth : 600 ,
129+ width : 'fit-content' ,
130+ maxWidth : 'none' ,
131+ } }
132+ className = "whitespace-pre break-normal"
133+ wrapLongLines = { false }
134+ showLineNumbers = { window . innerWidth <= 640 }
135+ >
136+ { codeString }
137+ </ SyntaxHighlighter >
138+ </ div >
139+ < button
140+ type = "button"
141+ onClick = { ( ) => copyToClipboard ( codeString ) }
142+ className = "absolute top-2 right-2 bg-gray-700 text-white px-2 py-1 text-xs rounded-md hover:bg-gray-600 transition"
143+ style = { {
144+ fontSize : window . innerWidth <= 640 ? '0.7rem' : '0.8rem' ,
145+ padding : window . innerWidth <= 640 ? '2px 6px' : undefined ,
145146 } }
146- className = "whitespace-pre break-normal"
147- wrapLongLines = { false }
148- showLineNumbers = { window . innerWidth <= 640 }
149147 >
150- { codeString }
151- </ SyntaxHighlighter >
148+ { copied ? "✅ 복사됨" : "📋 복사" }
149+ </ button >
152150 </ div >
153- < button
154- type = "button"
155- onClick = { ( ) => copyToClipboard ( codeString ) }
156- className = "absolute top-2 right-2 bg-gray-700 text-white px-2 py-1 text-xs rounded-md hover:bg-gray-600 transition"
157- style = { {
158- fontSize : window . innerWidth <= 640 ? '0.7rem' : '0.8rem' ,
159- padding : window . innerWidth <= 640 ? '2px 6px' : undefined ,
160- } }
161- >
162- { copied ? "✅ 복사됨" : "📋 복사" }
163- </ button >
164- </ div >
165- ) ;
166- } ,
167- a : ( { node, ...props } ) => (
168- < a
169- style = { { color : "lightblue" } }
170- target = "_blank"
171- rel = "noopener noreferrer"
172- className = "max-sm:text-[0.8rem]"
173- { ...props }
174- />
175- ) ,
176- img : ( { node, ...props } ) => < img style = { { maxWidth : "100%" , borderRadius : "8px" } } { ...props } /> ,
177- } }
178- >
179- { String ( text ) }
180- </ ReactMarkdown >
181- { /* 꼬리표를 메시지 박스 뒤에 출력 */ }
182- { ! isIntroMessage && (
183- user === "나" ? (
184- < div
185- className = "absolute right-[-12px] bottom-2 w-0 h-0 border-t-[12px] border-b-[12px] border-l-[14px]"
186- style = { {
187- borderTopColor : "transparent" ,
188- borderBottomColor : "transparent" ,
189- borderLeftColor : messageBgColor || "currentColor"
190- } }
191- > </ div >
192- ) : (
193- < div
194- className = "absolute left-[-12px] bottom-2 w-0 h-0 border-t-[12px] border-b-[12px] border-r-[14px]"
195- style = { {
196- borderTopColor : "transparent" ,
197- borderBottomColor : "transparent" ,
198- borderRightColor : messageBgColor || "currentColor"
199- } }
200- > </ div >
201- )
151+ ) ;
152+ } ,
153+ a : ( { node, ...props } ) => (
154+ < a
155+ className = { `underline hover:no-underline transition-all max-sm:text-[0.8rem] ${ user === "나" ? "text-blue-400" : "text-blue-300" } ` }
156+ target = "_blank"
157+ rel = "noopener noreferrer"
158+ { ...props }
159+ />
160+ ) ,
161+ img : ( { node, ...props } ) => < img className = "max-w-full rounded-lg my-2" alt = "" { ...props } /> ,
162+ } }
163+ >
164+ { String ( text ) }
165+ </ ReactMarkdown >
166+ </ div >
167+
168+ { /* 꼬리표를 유저 메시지에만 표시 */ }
169+ { ! isIntroMessage && user === "나" && (
170+ < div
171+ className = "absolute right-[-12px] bottom-2 w-0 h-0 border-t-[12px] border-b-[12px] border-l-[14px]"
172+ style = { {
173+ borderTopColor : "transparent" ,
174+ borderBottomColor : "transparent" ,
175+ borderLeftColor : messageBgColor || "currentColor"
176+ } }
177+ > </ div >
202178 ) }
203179 </ div >
204180 ) ;
0 commit comments