init
This commit is contained in:
53
README.md
53
README.md
@@ -87,6 +87,59 @@ npm run build
|
|||||||
npm run preview # http://localhost:4173
|
npm run preview # http://localhost:4173
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 프로덕션 배포 (Apache)
|
||||||
|
이 프로젝트는 기본적으로 API 호출을 `API_BASE_URL` 기준으로 수행합니다.
|
||||||
|
|
||||||
|
- `VITE_API_BASE_URL`를 지정하면 해당 주소로 호출합니다.
|
||||||
|
- 미지정 시:
|
||||||
|
- 개발( `npm run dev` )에서는 `http://localhost:8010`
|
||||||
|
- 프로덕션 빌드에서는 **same-origin**(현재 접속한 도메인)으로 호출합니다.
|
||||||
|
|
||||||
|
즉, Apache로 프론트를 HTTPS로 서비스하는 경우에는 아래처럼 **리버스 프록시**로 백엔드(예: `http://127.0.0.1:8010`)를 연결하는 방식을 권장합니다.
|
||||||
|
|
||||||
|
### Apache 모듈 활성화
|
||||||
|
```bash
|
||||||
|
sudo a2enmod proxy proxy_http rewrite
|
||||||
|
sudo systemctl reload apache2
|
||||||
|
```
|
||||||
|
|
||||||
|
### VirtualHost 예시 (프론트 정적 + API 프록시)
|
||||||
|
```apache
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName your-domain.com
|
||||||
|
|
||||||
|
DocumentRoot /var/www/ncuetalk_frontend
|
||||||
|
|
||||||
|
<Directory /var/www/ncuetalk_frontend>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# SPA 라우팅
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
|
||||||
|
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
|
||||||
|
RewriteRule ^ - [L]
|
||||||
|
RewriteRule ^ /index.html [L]
|
||||||
|
|
||||||
|
# Backend reverse proxy (HTTP backend on :8010)
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /auth http://127.0.0.1:8010/auth
|
||||||
|
ProxyPassReverse /auth http://127.0.0.1:8010/auth
|
||||||
|
|
||||||
|
ProxyPass /community http://127.0.0.1:8010/community
|
||||||
|
ProxyPassReverse /community http://127.0.0.1:8010/community
|
||||||
|
|
||||||
|
ProxyPass /chat http://127.0.0.1:8010/chat
|
||||||
|
ProxyPassReverse /chat http://127.0.0.1:8010/chat
|
||||||
|
|
||||||
|
ProxyPass /tools http://127.0.0.1:8010/tools
|
||||||
|
ProxyPassReverse /tools http://127.0.0.1:8010/tools
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
백엔드 엔드포인트가 더 있다면 동일 패턴으로 `ProxyPass`를 추가하거나, `/api` prefix로 백엔드를 묶어 프론트 코드에서 `VITE_API_BASE_URL=/api`로 통일하는 것도 가능합니다.
|
||||||
|
|
||||||
## 빌드 산출물 구조
|
## 빌드 산출물 구조
|
||||||
프로덕션 빌드시 `dist/` 폴더가 생성되며, 정적 파일을 Nginx·S3 등에 바로 서빙할 수 있습니다.
|
프로덕션 빌드시 `dist/` 폴더가 생성되며, 정적 파일을 Nginx·S3 등에 바로 서빙할 수 있습니다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
export const API_BASE_URL =
|
/**
|
||||||
import.meta.env.VITE_API_BASE_URL ||
|
* API base URL strategy
|
||||||
`${window.location.protocol}//${window.location.hostname}:8010`;
|
* - If `VITE_API_BASE_URL` is provided, always use it.
|
||||||
|
* - In production, default to same-origin (expects Apache/Nginx reverse proxy).
|
||||||
|
* - In dev, default to local backend.
|
||||||
|
*/
|
||||||
|
export const API_BASE_URL = (() => {
|
||||||
|
const fromEnv = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
if (fromEnv) return fromEnv.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
// Vite replaces `import.meta.env.PROD/DEV` at build time.
|
||||||
|
if (import.meta.env.PROD) return window.location.origin;
|
||||||
|
|
||||||
|
return 'http://localhost:8010';
|
||||||
|
})();
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { API_BASE_URL } from '../config';
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
||||||
|
function normalizeExternalUrl(raw) {
|
||||||
|
if (!raw) return '';
|
||||||
|
const s = String(raw).trim();
|
||||||
|
if (!s) return '';
|
||||||
|
// already absolute
|
||||||
|
if (/^https?:\/\//i.test(s)) return s;
|
||||||
|
// protocol-relative
|
||||||
|
if (s.startsWith('//')) return `https:${s}`;
|
||||||
|
// common "missing scheme" cases from DB/meta
|
||||||
|
if (s.startsWith('img.youtube.com/')) return `https://${s}`;
|
||||||
|
if (s.startsWith('www.youtube.com/')) return `https://${s}`;
|
||||||
|
if (s.startsWith('youtube.com/')) return `https://${s}`;
|
||||||
|
return s; // leave as-is (could be relative path on same origin)
|
||||||
|
}
|
||||||
|
|
||||||
export default function AiNewsPage() {
|
export default function AiNewsPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [news, setNews] = useState([]);
|
const [news, setNews] = useState([]);
|
||||||
@@ -101,10 +116,20 @@ export default function AiNewsPage() {
|
|||||||
<div key={n.id} className="card" style={{ marginBottom: 12 }}>
|
<div key={n.id} className="card" style={{ marginBottom: 12 }}>
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
<div style={{ display: 'flex', gap: 12 }}>
|
||||||
{n.meta?.image ? (
|
{n.meta?.image ? (
|
||||||
<img src={n.meta.image} alt="thumb" style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 8 }} />
|
<img
|
||||||
|
src={normalizeExternalUrl(n.meta.image)}
|
||||||
|
alt="thumb"
|
||||||
|
style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 8 }}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<a href={n.meta?.url || n.url} target="_blank" rel="noreferrer" style={{ fontWeight: 600, color: '#1976d2', textDecoration: 'none' }}>
|
<a
|
||||||
|
href={normalizeExternalUrl(n.meta?.url || n.url)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
style={{ fontWeight: 600, color: '#1976d2', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
{n.meta?.title || n.url}
|
{n.meta?.title || n.url}
|
||||||
</a>
|
</a>
|
||||||
<div style={{ marginTop: 6, color: '#555', lineHeight: 1.5 }}>{n.meta?.description}</div>
|
<div style={{ marginTop: 6, color: '#555', lineHeight: 1.5 }}>{n.meta?.description}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user