Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/web/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# HTTPS 개발 서버 (JavaScript 파일)
server.mjs

# Next.js 빌드 출력
.next/
out/

# 의존성
node_modules/

# 생성된 파일
src/generated/
2 changes: 1 addition & 1 deletion apps/web/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = {
parserOptions: {
project: './tsconfig.json',
},
ignorePatterns: ['stylelint.config.js', 'postcss.config.mjs', '**/__tests__/e2e/**'],
ignorePatterns: ['stylelint.config.js', 'postcss.config.mjs', '**/__tests__/e2e/**', 'server.mjs'],
extends: [
'next',
'next/core-web-vitals',
Expand Down
32 changes: 32 additions & 0 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ const nextConfig = {
typescript: { ignoreBuildErrors: false },
eslint: { ignoreDuringBuilds: false },
output: 'standalone',
// 인증 관련 엔드포인트만 프록시 (쿠키 공유를 위해)
async rewrites() {
// HTTPS 환경에서만 프록시 활성화
// HTTP 개발 환경(CSR only)에서는 프록시 비활성화
const proxyEnabled =
process.env.NEXT_PUBLIC_ENABLE_PROXY !== 'false';

if (!proxyEnabled) {
console.log(
'[Next.js] 🚫 프록시 비활성화 - HTTP CSR 전용 모드',
);
Comment on lines +22 to +24
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빌드 시마다 프록시 비활성화 로그가 출력되면 불필요한 로그가 발생합니다. 개발 환경에서만 로깅하는 것이 좋습니다.

해결 방법:

if (!proxyEnabled) {
  if (process.env.NODE_ENV === 'development') {
    console.log(
      '[Next.js] 🚫 프록시 비활성화 - HTTP CSR 전용 모드',
    );
  }
  return [];
}
Suggested change
console.log(
'[Next.js] 🚫 프록시 비활성화 - HTTP CSR 전용 모드',
);
if (process.env.NODE_ENV === 'development') {
console.log(
'[Next.js] 🚫 프록시 비활성화 - HTTP CSR 전용 모드',
);
}

Copilot uses AI. Check for mistakes.
return [];
}

const apiBaseUrl =
process.env.NEXT_PUBLIC_API_BASE_URL ||
'https://soso.dreampaste.com';

console.log('[Next.js] ✅ 프록시 활성화 - HTTPS 전체 기능 모드');
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빌드 시마다 프록시 활성화 로그가 출력되면 불필요한 로그가 발생합니다.

해결 방법:

if (process.env.NODE_ENV === 'development') {
  console.log('[Next.js] ✅ 프록시 활성화 - HTTPS 전체 기능 모드');
}
Suggested change
console.log('[Next.js] ✅ 프록시 활성화 - HTTPS 전체 기능 모드');
if (process.env.NODE_ENV === 'development') {
console.log('[Next.js] ✅ 프록시 활성화 - HTTPS 전체 기능 모드');
}

Copilot uses AI. Check for mistakes.
return [
// 인증 엔드포인트 (로그인, 토큰 갱신)
{
source: '/api/auth/:path*',
destination: `${apiBaseUrl}/auth/:path*`,
},
// 현재 유저 정보 (SSR prefetch)
{
source: '/api/users/me',
destination: `${apiBaseUrl}/users/me`,
},
];
},
//추후 제거 필요
images: {
domains: [
Expand Down
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"dev": "NEXT_PUBLIC_ENABLE_PROXY=false next dev",
"dev:https": "NEXT_PUBLIC_ENABLE_PROXY=true node server.mjs",
"build": "next build",
"build:analyze": "ANALYZE=true next build",
"start": "next start",
Expand Down
42 changes: 42 additions & 0 deletions apps/web/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createServer } from 'https';
import { parse } from 'url';
import next from 'next';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = 3000;

// SSL 인증서 로드
const httpsOptions = {
key: fs.readFileSync(join(__dirname, '.cert', 'localhost+2-key.pem')),
cert: fs.readFileSync(join(__dirname, '.cert', 'localhost+2.pem')),
};
Comment on lines +16 to +19
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSL 인증서 경로가 하드코딩되어 있어 다른 경로에 인증서를 생성한 경우 동작하지 않습니다. 환경 변수나 여러 경로를 시도하는 방식으로 유연성을 높이는 것이 좋습니다.

제안:

const certDir = process.env.SSL_CERT_DIR || join(__dirname, '.cert');
const httpsOptions = {
  key: fs.readFileSync(join(certDir, 'localhost+2-key.pem')),
  cert: fs.readFileSync(join(certDir, 'localhost+2.pem')),
};

또는 여러 파일명 패턴을 시도:

const certFiles = ['localhost+2', 'localhost', 'local'];
for (const name of certFiles) {
  try {
    const httpsOptions = {
      key: fs.readFileSync(join(__dirname, '.cert', `${name}-key.pem`)),
      cert: fs.readFileSync(join(__dirname, '.cert', `${name}.pem`)),
    };
    break;
  } catch (e) {
    // 다음 패턴 시도
  }
}
Suggested change
const httpsOptions = {
key: fs.readFileSync(join(__dirname, '.cert', 'localhost+2-key.pem')),
cert: fs.readFileSync(join(__dirname, '.cert', 'localhost+2.pem')),
};
const certDir = process.env.SSL_CERT_DIR || join(__dirname, '.cert');
const certFiles = ['localhost+2', 'localhost', 'local'];
let httpsOptions;
for (const name of certFiles) {
try {
httpsOptions = {
key: fs.readFileSync(join(certDir, `${name}-key.pem`)),
cert: fs.readFileSync(join(certDir, `${name}.pem`)),
};
break;
} catch (e) {
// 다음 패턴 시도
}
}
if (!httpsOptions) {
throw new Error(
`SSL 인증서 파일을 찾을 수 없습니다. ${certDir} 경로에 인증서 파일이 있는지 확인하세요.`
);
}

Copilot uses AI. Check for mistakes.

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
createServer(httpsOptions, async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
Comment on lines +32 to +33
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 메시지가 너무 간단하여 디버깅에 도움이 되지 않습니다. 에러 상세 정보를 포함하는 것이 좋습니다.

제안:

} catch (err) {
  console.error('Error occurred handling', req.url, err);
  res.statusCode = 500;
  res.end(`Internal server error: ${err.message || 'Unknown error'}`);
}
Suggested change
res.end('internal server error');
}
if (dev) {
res.end(`Internal server error: ${err.message || 'Unknown error'}`);
} else {
res.end('internal server error');
}

Copilot uses AI. Check for mistakes.
})
.once('error', (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on https://${hostname}:${port}`);
});
});
28 changes: 13 additions & 15 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
// apps/web/app/layout.tsx
import type { Metadata } from 'next';
import './globals.css';
import type { Metadata, Viewport } from 'next';
import { QueryProvider } from '@/providers/queryProvider';
import { AuthProvider } from '@/providers/AuthProvider';
import pretendardFont from '@/assets/fonts/PretandardFont';
import { ToastContainer } from '@/components/toast/ToastContainer';
import { OverlayPortal } from '@/components/overlayPortal';
import { AuthHydrationProvider } from '@/providers/AuthHydrationProvider';

export const metadata: Metadata = {
title: 'SoSo – Local Biz Helper',
description: '지역 주민과 함께 만드는 창업 플랫폼',
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
themeColor: '#4CAF50', //tailwindcss green-500
//manifest: '/manifest.json',
keywords: ['창업', '지역', '소상공인', '아이디어', '투표'],
authors: [{ name: 'SOSO Team' }],
appleWebApp: {
Expand All @@ -35,7 +26,14 @@ export const metadata: Metadata = {
locale: 'ko_KR',
},
};
// 전역 폰트 설정

export const viewport: Viewport = {
themeColor: '#4CAF50',
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
};

export default function RootLayout({
children,
Expand All @@ -55,14 +53,14 @@ export default function RootLayout({
className="flex flex-col h-screen bg-gradient-to-br from-white to-white dark:from-neutral-1000 dark:to-neutral-900"
>
<QueryProvider>
<AuthProvider>
{/* */}
<AuthHydrationProvider>
{/* AuthProvider는 AuthHydration 내부에 포함됨 */}
<main className="w-full h-full max-w-screen-md md:mx-auto flex-1 overflow-auto">
{children}
<ToastContainer />
</main>
<OverlayPortal />
</AuthProvider>
</AuthHydrationProvider>
</QueryProvider>
</body>
</html>
Expand Down
35 changes: 14 additions & 21 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
// apps/web/src/app/page.tsx
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/stores/authStore';
import { useAuth } from '@/hooks/useAuth';
import LogoImage from '@/assets/images/LogoImage';
import Button from '@/components/buttons/Button';

export default function HomePage() {
const router = useRouter();
const { getIsAuth, isLoading } = useAuthStore();
const [ready, setReady] = useState(false);

useEffect(() => {
if (!isLoading) setReady(true);
}, [isLoading]);
const { isAuth, isLoading } = useAuth();

const handleStart = () => {
router.replace(getIsAuth() ? '/main' : '/login');
router.replace(isAuth ? '/main' : '/login');
};

return (
<div className="h-full w-full flex flex-col items-center justify-between pt-40">
<h1 className="text-2xl font-semibold animate-fadeIn text-neutral-900 dark:text-neutral-100">
{ready ? '소소에 오신 것을 환영합니다' : ''}
소소에 오신 것을 환영합니다
</h1>
<div className="w-full h-full p-layout flex animate-fadeIn flex-col items-center justify-between space-y-4">
<div className="my-20 flex-1 flex items-center justify-center">
Expand All @@ -32,17 +25,17 @@ export default function HomePage() {

<div className="w-full space-y-4 flex flex-col items-center">
<p className="text-neutral-600 animate-pulse dark:text-neutral-400 text-sm">
{ready ? '일상의 소소한 순간들을 기록해보세요' : ' '}
일상의 소소한 순간들을 기록해보세요
</p>
{ready && (
<Button
onClick={handleStart}
variant="filled"
className="w-full"
>
소소 시작하기
</Button>
)}

<Button
onClick={handleStart}
variant="filled"
className="w-full"
isLoading={isLoading}
>
소소 시작하기
</Button>
</div>
</div>
</div>
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/generated/api/endpoints/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ import { customInstance } from '../../../../lib/api-client';
- 새로운 Access Token, Refresh Token 발급

**토큰 발급 방식:**
- Response Body: accessToken 포함 (기존 호환성 유지)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 설정 (SSR 지원)
- Response Body: accessToken 포함 (모바일 앱 지원)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 갱신 (웹 브라우저 자동 관리)

**쿠키 보안 속성:**
- accessToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어)
- refreshToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어)

**클라이언트별 사용 방법:**
- 웹 브라우저: 쿠키 자동 갱신, 별도 처리 불필요
- 모바일 앱: Body에서 새 accessToken 추출 후 저장소 업데이트

* @summary Access Token 재발급
*/
Expand Down Expand Up @@ -196,8 +204,16 @@ export const useLogout = <TError = void, TContext = unknown>(
* 카카오 인가 코드를 통해 사용자 로그인을 처리합니다.

**토큰 발급 방식:**
- Response Body: accessToken 포함 (기존 호환성 유지)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 설정 (SSR 지원)
- Response Body: accessToken 포함 (모바일 앱 지원)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 설정 (웹 브라우저 자동 관리)

**쿠키 보안 속성:**
- accessToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어, 30분)
- refreshToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어, 14일)

**클라이언트별 사용 방법:**
- 웹 브라우저: 쿠키 자동 관리, credentials: 'include' 설정 필요
- 모바일 앱: Body에서 accessToken 추출 후 AsyncStorage/SharedPreferences 저장

**기존 사용자:** JWT 토큰 및 사용자 정보 반환
**신규 사용자:** 회원가입 세션 생성
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/generated/api/endpoints/freeboard/freeboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,6 @@ export type UpdateFreeboardPostMutationError =
| ErrorResponse
| ErrorResponse
| ErrorResponse
| ErrorResponse
| ErrorResponse
| ErrorResponse;

/**
Expand Down
14 changes: 9 additions & 5 deletions apps/web/src/generated/api/endpoints/signup/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,12 +557,16 @@ export const useSetExperience = <
* 회원가입을 완료하고 사용자 계정을 생성합니다.

**토큰 발급 방식:**
- Response Body: accessToken 포함 (기존 호환성 유지)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 설정 (SSR 지원)
- Response Body: accessToken 포함 (모바일 앱 지원)
- Set-Cookie 헤더: accessToken, refreshToken 쿠키 설정 (웹 브라우저 자동 관리)

**쿠키 속성:**
- accessToken: HttpOnly=false (JavaScript 접근 가능, 30분)
- refreshToken: HttpOnly=true (XSS 보호, 7일)
**쿠키 보안 속성:**
- accessToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어, 30분)
- refreshToken: HttpOnly=true, Secure=true, SameSite=None (XSS 방어, 14일)

**클라이언트별 사용 방법:**
- 웹 브라우저: 쿠키 자동 관리, credentials: 'include' 설정 필요
- 모바일 앱: Body에서 accessToken 추출 후 AsyncStorage/SharedPreferences 저장

* @summary [9단계] 회원가입 완료
*/
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/generated/api/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export * from './userResponseAgeRange';
export * from './userResponseGender';
export * from './userResponseUserType';
export * from './userSummaryResponse';
export * from './userSummaryResponseUserType';
export * from './userSummaryResponse';
export * from './userSummaryResponseUserType';
export * from './userSummaryResponseUserType';
export * from './userTypeRequest';
export * from './userTypeRequestUserType';
export * from './voteOptionRequest';
Expand Down
Loading