diff --git a/next.config.mjs b/next.config.mjs index ae9fc4d..dec7931 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,6 +8,12 @@ const nextConfig = { dangerouslyAllowSVG: true, contentDispositionType: "attachment", contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + remotePatterns: [ + { + protocol: "https", + hostname: "*.s3.ap-northeast-2.amazonaws.com", + }, + ], }, }; diff --git a/package.json b/package.json index 3eec757..6279d0c 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "npm run open-browser && concurrently --raw \"npm run generate-api-routers\" \"next dev --experimental-https\"", + "dev": "npm run set-domain && npm run open-browser && next dev -H local.fineants.co --experimental-https", "open-browser": "vite-node ./scripts/open-browser.ts", + "set-domain": "node ./scripts/set-domain.mjs", "generate-api-routers": "vite-node ./scripts/proxy-api-router-generator.ts", "build": "next build", "start": "next start", @@ -22,7 +23,6 @@ "@mui/x-date-pickers": "^7.22.3", "@tanstack/react-query": "^5.55.4", "@tanstack/react-query-devtools": "^5.59.15", - "chokidar": "^4.0.1", "dayjs": "^1.11.13", "lightweight-charts": "^4.2.1", "next": "14.2.10", diff --git a/scripts/open-browser.ts b/scripts/open-browser.ts index e977fd2..5a533fd 100644 --- a/scripts/open-browser.ts +++ b/scripts/open-browser.ts @@ -1,7 +1,7 @@ import { exec } from "child_process"; import os from "os"; -const url = "https://localhost:3000"; +const url = "https://local.fineants.co:3000"; if (os.platform() === "win32") { exec(`start ${url}`); diff --git a/scripts/proxy-api-router-generator.ts b/scripts/proxy-api-router-generator.ts deleted file mode 100644 index 18b6739..0000000 --- a/scripts/proxy-api-router-generator.ts +++ /dev/null @@ -1,98 +0,0 @@ -import chokidar from "chokidar"; -import fs from "fs"; -import path from "path"; -import { generateApiRouter } from "./utils/api-router-generator-utils"; - -const watchPath = path.resolve("./src/features"); -const proxyPath = path.resolve("./src/pages/api/proxy"); - -// ./src/features/api 아래에 있는 index.ts 체크 -function isApiIndexFile(filePath: string): boolean { - return filePath.endsWith(path.join("api", "index.ts")); -} - -// 초기화: ./src/pages/api/proxy 경로 삭제 후 생성 -function initializeProxyPath() { - fs.rmSync(proxyPath, { recursive: true, force: true }); - fs.mkdirSync(proxyPath, { recursive: true }); // proxyPath를 재생성 - - console.log(`Generator: Cleared and recreated directory: ${proxyPath}`); -} - -// 전체 재생성: watchPath 내부의 모든 index.ts 파일을 다시 생성 -function regenerateAllApiRouters() { - const files = getAllIndexFiles(watchPath); - - files.forEach((filePath) => { - console.log(`Generator: Regenerating API router for: ${filePath}`); - generateApiRouter(filePath); - }); -} - -// watchPath 내부의 모든 index.ts 파일 경로를 가져오는 함수 -function getAllIndexFiles(dir: string): string[] { - let results: string[] = []; - const list = fs.readdirSync(dir); - - list.forEach((file) => { - const filePath = path.resolve(dir, file); - const stat = fs.statSync(filePath); - - if (stat && stat.isDirectory()) { - results = results.concat(getAllIndexFiles(filePath)); - } else if (isApiIndexFile(filePath)) { - results.push(filePath); - } - }); - return results; -} - -// 초기화 함수 실행 -initializeProxyPath(); - -// chokidar 설정: 파일 변경 감시 -const watcher = chokidar.watch(watchPath, { - persistent: true, - depth: 3, - awaitWriteFinish: { - stabilityThreshold: 2000, - pollInterval: 100, - }, -}); - -// 준비 완료 -watcher.on("ready", () => { - console.log("Generator: Watching for file changes..."); -}); - -// 파일 변경 -watcher.on("change", (filePath: string) => { - if (isApiIndexFile(filePath)) { - console.log(`Generator: File changed: ${filePath}`); - - generateApiRouter(filePath); - } -}); - -// 파일 추가 -watcher.on("add", (filePath: string) => { - if (isApiIndexFile(filePath)) { - console.log(`Generator: File added: ${filePath}`); - - generateApiRouter(filePath); - } -}); - -// 파일 삭제 -watcher.on("unlink", (filePath: string) => { - if (isApiIndexFile(filePath)) { - console.log(`Generator: File deleted: ${filePath}`); - - initializeProxyPath(); - regenerateAllApiRouters(); - - console.log( - "Generator: Re-initialized proxy path and regenerated all API routers due to deletion." - ); - } -}); diff --git a/scripts/set-domain.mjs b/scripts/set-domain.mjs new file mode 100644 index 0000000..78ce8f9 --- /dev/null +++ b/scripts/set-domain.mjs @@ -0,0 +1,69 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import os from "os"; + +const HOSTS_FILE = + os.platform() === "win32" + ? "C:\\Windows\\System32\\drivers\\etc\\hosts" + : "/etc/hosts"; +const DOMAIN = "local.fineants.co"; +const IP_ADDRESS = "127.0.0.1"; +const ENTRY = `${IP_ADDRESS} ${DOMAIN}`; + + +// 관리자 권한 여부 확인 함수 +function isAdmin() { + try { + if (os.platform() === "win32") { + execSync("net session", { stdio: "ignore" }); + return true; + } else { + return process.getuid && process.getuid() === 0; + } + } catch (err) { + return false; + } +} + +// hosts 파일에서 특정 도메인이 존재하는지 확인하는 함수 +function isDomainInHosts(domain) { + try { + const content = fs.readFileSync(HOSTS_FILE, "utf8"); + return content.split("\n").some((line) => line.includes(domain)); + } catch (err) { + console.error("❌ hosts 파일을 읽는 중 오류가 발생했습니다:", err); + return false; + } +} + +// hosts 파일에 도메인 추가 +function appendToHosts() { + try { + fs.appendFileSync(HOSTS_FILE, `\n${ENTRY}\n`); + console.log(`✅ ${DOMAIN} 도메인을 hosts 파일에 성공적으로 추가했습니다.`); + } catch (err) { + console.error("❌ hosts 파일을 수정하는 중 오류가 발생했습니다:", err); + } +} + +function run() { + if (isDomainInHosts(DOMAIN)) { + console.log(`✅ ${DOMAIN} 도메인이 이미 hosts 파일에 등록되어 있습니다.`); + return; + } + + console.log(`⚠️ ${DOMAIN} 도메인이 hosts 파일에 없습니다. 추가를 시도합니다.`); + + if (isAdmin()) { + appendToHosts(); + } else { + console.log("⚠️ hosts 파일을 수정하려면 관리자 권한이 필요합니다."); + if (os.platform() === "win32") { + console.log("🔒 관리자 권한으로 이 스크립트를 실행해주세요."); + } else { + console.log(`🔒 다음 명령어를 사용해 sudo로 실행해주세요:\n sudo node ${process.argv[1]}`); + } + } +} + +run(); diff --git a/scripts/utils/api-router-generator-utils.ts b/scripts/utils/api-router-generator-utils.ts deleted file mode 100644 index 2702383..0000000 --- a/scripts/utils/api-router-generator-utils.ts +++ /dev/null @@ -1,163 +0,0 @@ -import fs from "fs"; -import path from "path"; - -type MethodDetails = { - url: string | null; - httpMethod: string | null; -}; - -// API 라우터 생성 함수 -export const generateApiRouter = (filePath: string) => { - const content = fs.readFileSync(filePath, "utf-8"); - - const apiMethods = extractApiMethods(content); - const methodDetails = extractMethodDetails(apiMethods); - - createApiFiles(methodDetails); -}; - -// 파일 생성 함수 -const createApiFiles = (methodDetails: MethodDetails[]) => { - const groupedByRoute = methodDetails.reduce( - (acc, { url, httpMethod }) => { - if (!url || !httpMethod) return acc; - if (!acc[url]) acc[url] = []; - acc[url].push(httpMethod); - return acc; - }, - {} as Record - ); - - Object.entries(groupedByRoute).forEach(([url, methods]) => { - const urlParts = url - .split("/") - .filter((part) => part !== "") - .map((part) => (part.startsWith("${") ? `[${part.slice(2, -1)}]` : part)); - - const filePath = path.join("./src/pages/api/proxy", ...urlParts) + ".ts"; - - // 파일 디렉토리 없을 경우 생성 - const dirPath = path.dirname(filePath); - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } - - // 템플릿 리터럴이 있는지 확인 ex) `/stocks/${tickerSymbol}` - const templateVariables = url.match(/\$\{(\w+)\}/g); - const queryExtract = templateVariables - ? templateVariables - .map((variable) => `const { ${variable.slice(2, -1)} } = req.query;`) - .join("\n") - : ""; - - // Method 별 처리 - const methodHandlers = methods - .map((method) => { - const hasBody = ["POST", "PUT", "PATCH", "DELETE"].includes(method); - const bodyContent = hasBody ? ", req.body" : ""; - - return ` - case "${method}": - try { - const { data, cookies: { accessToken, refreshToken } = {} } = - await proxyFetcher.${method.toLowerCase()}>(\`${url.replace( - /\$\{(\w+)\}/g, - (_, variable) => `\${${variable}}` - )}\`${bodyContent}, { - headers: { - Cookie: cookieString || "", - }, - }); - - if (accessToken && refreshToken) { - const updatedAccessToken = replaceCookieDomain( - accessToken, - "localhost" - ); - const updatedRefreshToken = replaceCookieDomain( - refreshToken, - "localhost" - ); - - res.setHeader("Set-Cookie", [updatedAccessToken, updatedRefreshToken]); - } - - return res.status(200).json(data); - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } - `; - }) - .join("\n"); - - // 템플릿 코드 - const template = `import { proxyFetcher } from "@/api/fetcher"; -import { Response } from "@/api/types"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const cookies = req.cookies; - const cookieString = Object.entries(cookies) - .map(([key, value]) => \`\${key}=\${value}\`) - .join("; "); - - ${queryExtract} - - switch (req.method) { - ${methodHandlers} - default: - res.setHeader("Allow", [${methods.map((m) => `"${m}"`).join(", ")}]); - res.status(405).end(\`Method \${req.method} Not Allowed\`); - } -} - -const replaceCookieDomain = (cookieStr: string, newDomain: string) => { - const cookieParts = cookieStr.split(";"); - const updatedCookieParts = cookieParts.map((part) => { - if (part.trim().startsWith("Domain=")) { - return \` Domain=\${newDomain}\`; - } - return part; - }); - return updatedCookieParts.join(";"); -}; - `; - - fs.writeFileSync(filePath, template, "utf8"); - console.log(`Generator: Created API file: ${filePath}`); - }); -}; - -// 메서드 추출 함수 -export const extractApiMethods = (content: string): string[] => { - const methodRegex = - /export\s+const\s+\w+\s*=\s*async\s*\([^)]*\)\s*=>\s*\{[\s\S]*?\};/g; - const methods: string[] = []; - let match; - - while ((match = methodRegex.exec(content)) !== null) { - methods.push(match[0]); - } - - return methods; -}; - -// 메서드 상세 정보 추출 함수 -const extractMethodDetails = (apiMethods: string[]): MethodDetails[] => { - return apiMethods.map((method) => { - const urlMatch = method.match(/["'`](\/[^"'`]+)["'`]/); - const url = urlMatch ? urlMatch[1] : null; - - const httpMethodMatch = method.match( - /(?:fetcher|fetcherWithoutCredentials|proxyFetcher)\.(\w+)/ - ); - const httpMethod = httpMethodMatch - ? httpMethodMatch[1].toUpperCase() - : null; - - return { - url, - httpMethod, - }; - }); -}; diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index 34038c9..6a90c6e 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -9,25 +9,6 @@ type FetcherOptions = RequestInit & { type FetcherResponse = { data: T; - cookies: { - accessToken: string | undefined; - refreshToken: string | undefined; - }; -}; - -const getCookies = (response: Response) => { - const setCookieHeader = response.headers.get("set-cookie"); - - const cookies = setCookieHeader?.split(", ") || []; - - const accessToken = cookies.find((cookie) => - cookie.startsWith("accessToken") - ); - const refreshToken = cookies.find((cookie) => - cookie.startsWith("refreshToken") - ); - - return { accessToken, refreshToken }; }; const handleError = async (response: Response) => { @@ -83,7 +64,7 @@ const requestWithoutData = async ( const json = await response.json(); - return { data: json, cookies: getCookies(response) }; + return { data: json }; }; // 데이터를 받는 요청 (POST, PUT, PATCH) @@ -114,7 +95,7 @@ const requestWithData = async ( const json = await response.json(); - return { data: json, cookies: getCookies(response) }; + return { data: json }; }; const createFetcher = ( @@ -169,16 +150,7 @@ const createFetcher = ( }; }; -const fetcherBaseURL = - process.env.NODE_ENV === "development" - ? `${CLIENT_URL}/api/proxy` - : `${BASE_API_URL}/api`; - -export const fetcher = createFetcher(fetcherBaseURL, { - credentials: "include", -}); - -export const proxyFetcher = createFetcher(`${BASE_API_URL}/api`, { +export const fetcher = createFetcher(`${BASE_API_URL}/api`, { credentials: "include", });