Skip to content

Commit ab74c34

Browse files
authored
Merge pull request #55 from TreeNut-KR/develop
Develop
2 parents 6a0aaa4 + da02007 commit ab74c34

13 files changed

Lines changed: 506 additions & 197 deletions

File tree

mysql/migrations/V1.1.4__init.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE users
2+
MODIFY COLUMN login_type ENUM('LOCAL', 'KAKAO', 'GOOGLE', 'NAVER') DEFAULT 'LOCAL';

mysql/migrations/V1.1.5__init.sql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- 기존 소셜 로그인 사용자들을 VIP로 업데이트
2+
UPDATE users
3+
SET membership = 'VIP'
4+
WHERE login_type IN ('KAKAO', 'GOOGLE', 'NAVER');
5+
6+
-- 새로 가입하는 소셜 로그인 사용자들을 자동으로 VIP로 설정하는 트리거
7+
DELIMITER $$
8+
9+
CREATE TRIGGER update_membership_on_insert
10+
BEFORE INSERT ON users
11+
FOR EACH ROW
12+
BEGIN
13+
IF NEW.login_type IN ('KAKAO', 'GOOGLE', 'NAVER') THEN
14+
SET NEW.membership = 'VIP';
15+
END IF;
16+
END$$
17+
18+
CREATE TRIGGER update_membership_on_update
19+
BEFORE UPDATE ON users
20+
FOR EACH ROW
21+
BEGIN
22+
IF NEW.login_type IN ('KAKAO', 'GOOGLE', 'NAVER') THEN
23+
SET NEW.membership = 'VIP';
24+
ELSEIF NEW.login_type = 'LOCAL' AND OLD.login_type IN ('KAKAO', 'GOOGLE', 'NAVER') THEN
25+
SET NEW.membership = 'BASIC';
26+
END IF;
27+
END$$
28+
29+
DELIMITER ;

nginx/nginx.conf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ http {
142142
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
143143
add_header 'Access-Control-Allow-Headers' '*' always;
144144
}
145+
146+
# 네이버 로그인 처리
147+
location /server/user/social/naver/login {
148+
proxy_pass http://springboot:8080;
149+
proxy_set_header Host $host;
150+
proxy_set_header X-Real-IP $remote_addr;
151+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
152+
proxy_set_header X-Forwarded-Proto $scheme;
153+
154+
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
155+
add_header 'Access-Control-Allow-Credentials' 'true' always;
156+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
157+
add_header 'Access-Control-Allow-Headers' '*' always;
158+
}
159+
160+
# 네이버 OAuth 콜백 처리 (필요시)
161+
location /server/user/social/naver/redirect {
162+
proxy_pass http://springboot:8080;
163+
proxy_set_header Host $host;
164+
proxy_set_header X-Real-IP $remote_addr;
165+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
166+
proxy_set_header X-Forwarded-Proto $scheme;
167+
168+
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
169+
add_header 'Access-Control-Allow-Credentials' 'true' always;
170+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
171+
add_header 'Access-Control-Allow-Headers' '*' always;
172+
}
145173
}
146174
}
147175

nginx/react-frontpage/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const AppRoutes: React.FC = () => {
6565
<Route index element={<Login />} /> {/* / 경로에서 Login */}
6666
<Route path="loginMain" element={<Login />} />
6767
<Route path="/social/kakao/redirect" element={<KakaoCallback />} />
68+
<Route path="/social/naver/redirect" element={<Login />} />
6869
<Route path="/register" element={<Register />} /> {/* 회원가입 단독 페이지 */}
6970
</Route>
7071
</Routes>

nginx/react-frontpage/src/Component/Chatting/Chatting.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ const Chatting: React.FC<ChattingProps> = ({ messages, setMessages, onSend }) =>
576576

577577
<div
578578
className={[
579-
"flex flex-col text-white w-full max-w-3xl bg-gray-900",
579+
"flex flex-col text-white w-full max-w-5xl bg-gray-900",
580580
isIOS() ? "h-full" : "h-full"
581581
].join(" ")}
582582
style={isIOS() ? { minHeight: "100dvh", maxHeight: "100dvh" } : {}}

nginx/react-frontpage/src/Component/Chatting/Components/ChatMessage.tsx

Lines changed: 109 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)