diff --git a/package-lock.json b/package-lock.json index 5a69c849..a877bec0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "cookie": "^1.0.2", "date-fns": "^4.1.0", "mock-socket": "^9.3.1", - "next": "15.5.3", + "next": "^16.0.7", "react": "19.1.0", "react-dom": "19.1.0", "swiper": "^12.0.2", @@ -119,7 +119,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3416,9 +3415,9 @@ "license": "MIT" }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -3432,9 +3431,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", "cpu": [ "arm64" ], @@ -3448,9 +3447,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", "cpu": [ "x64" ], @@ -3464,9 +3463,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", "cpu": [ "arm64" ], @@ -3480,9 +3479,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", "cpu": [ "arm64" ], @@ -3496,9 +3495,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", "cpu": [ "x64" ], @@ -3512,9 +3511,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", "cpu": [ "x64" ], @@ -3528,9 +3527,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", "cpu": [ "arm64" ], @@ -3544,9 +3543,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", "cpu": [ "x64" ], @@ -4397,7 +4396,6 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4859,7 +4857,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz", "integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.7" }, @@ -4894,7 +4891,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5092,7 +5088,6 @@ "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5103,7 +5098,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5171,7 +5165,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5689,7 +5682,6 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -5827,7 +5819,6 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -5886,7 +5877,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6366,7 +6356,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6786,7 +6775,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7507,7 +7495,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7585,7 +7572,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7674,7 +7660,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7776,7 +7761,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10425,12 +10409,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -10440,18 +10424,18 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "16.0.7", + "@next/swc-darwin-x64": "16.0.7", + "@next/swc-linux-arm64-gnu": "16.0.7", + "@next/swc-linux-arm64-musl": "16.0.7", + "@next/swc-linux-x64-gnu": "16.0.7", + "@next/swc-linux-x64-musl": "16.0.7", + "@next/swc-win32-arm64-msvc": "16.0.7", + "@next/swc-win32-x64-msvc": "16.0.7", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -10886,7 +10870,6 @@ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright-core": "1.56.1" }, @@ -10968,7 +10951,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11163,7 +11145,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11218,7 +11199,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -11457,7 +11437,6 @@ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11906,7 +11885,6 @@ "integrity": "sha512-339U14K6l46EFyRvaPS2ZlL7v7Pb+LlcXT8KAETrGPxq8v1sAjj2HAOB6zrlAK3M+0+ricssfAwsLCwt7Eg8TQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -12665,7 +12643,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12863,7 +12840,6 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13024,7 +13000,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/package.json b/package.json index 33db738b..6b23f486 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "cookie": "^1.0.2", "date-fns": "^4.1.0", "mock-socket": "^9.3.1", - "next": "15.5.3", + "next": "^16.0.7", "react": "19.1.0", "react-dom": "19.1.0", "swiper": "^12.0.2", diff --git a/src/app/api/[...path]/route.ts b/src/app/api/[...path]/route.ts new file mode 100644 index 00000000..79450f65 --- /dev/null +++ b/src/app/api/[...path]/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from "next/server"; + +const BACKEND = process.env.NEXT_PUBLIC_API_URL as string; +const REFRESH_PATH = "/auth/refresh"; + +export const GET = proxy; +export const POST = proxy; +export const PUT = proxy; +export const PATCH = proxy; +export const DELETE = proxy; + +async function proxy( + req: NextRequest, + ctx: { params: Promise<{ path: string[] }> }, +) { + // 프록시 대상 경로 생성 + const { path: rawPath } = await ctx.params; + const path = "/" + rawPath.join("/"); + const backendURL = new URL(`${BACKEND}/api${path}`); + + req.nextUrl.searchParams.forEach((v, k) => { + backendURL.searchParams.set(k, v); + }); + + const headers = new Headers(req.headers); + headers.delete("host"); + headers.delete("content-length"); + + const accessToken = req.cookies.get("accessToken")?.value; + if (accessToken && path !== REFRESH_PATH) { + headers.set("Authorization", `Bearer ${accessToken}`); + } + // console.log(accessToken); + + // 실제 백엔드 요청 + const initialInit: RequestInit = { + method: req.method, + headers, + body: req.body, + redirect: "manual", + // @ts-expect-error — RequestInit 타입에는 없음 + duplex: "half", + }; + + let backendRes = await fetch(backendURL.toString(), initialInit); + + // 토큰 만료 → refresh 후 재요청 + if (backendRes.status === 401) { + const refreshToken = req.cookies.get("refreshToken")?.value ?? ""; + + const refreshRes = await fetch(`${BACKEND}${REFRESH_PATH}`, { + method: "POST", + headers: { Cookie: `refreshToken=${refreshToken}` }, + }); + + if (!refreshRes.ok) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } + + const { accessToken: newAccessToken } = await refreshRes.json(); + // console.log("Refreshed accessToken:", newAccessToken); + + const retryHeaders = new Headers(headers); + retryHeaders.set("Authorization", `Bearer ${newAccessToken}`); + + const retryInit: RequestInit = { + method: req.method, + headers: retryHeaders, + body: req.body, + redirect: "manual", + // @ts-expect-error -- duplex required for Node fetch streaming + duplex: "half", + }; + + backendRes = await fetch(backendURL.toString(), retryInit); + + // accessToken 쿠키 갱신 + const response = new NextResponse(backendRes.body, { + status: backendRes.status, + }); + + response.cookies.set({ + name: "accessToken", + value: newAccessToken, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60, + }); + + return response; + } + + // 일반 응답 그대로 반환 + return new NextResponse(backendRes.body, { + status: backendRes.status, + }); +} diff --git a/src/app/api/auth/refresh/route.ts b/src/app/api/auth/refresh/route.ts index b21bdd5d..c60c1817 100644 --- a/src/app/api/auth/refresh/route.ts +++ b/src/app/api/auth/refresh/route.ts @@ -2,29 +2,19 @@ import { cookies } from "next/headers"; import { NextResponse } from "next/server"; import * as cookie from "cookie"; -const BASE_URL = process.env.NEXT_PUBLIC_API_URL; +const BASE_URL = process.env.NEXT_PUBLIC_API_URL!; export async function POST() { - const cookieHandler = await cookies(); - const refreshToken = cookieHandler.get("refreshToken")?.value; + const cookieStore = await cookies(); + const refreshToken = cookieStore.get("refreshToken")?.value; if (!refreshToken) { - return NextResponse.json( - { message: "No refresh token provided" }, - { status: 401 }, - ); + return NextResponse.json({ message: "No refresh token" }, { status: 401 }); } - const cookieOptions = { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - path: "/", - }; - const backendRes = await fetch(`${BASE_URL}/auth/refresh`, { method: "POST", headers: { - "Content-Type": "application/json", Cookie: `refreshToken=${refreshToken}`, }, }); @@ -40,10 +30,12 @@ export async function POST() { const res = NextResponse.json({ accessToken }); - cookieHandler.set({ + res.cookies.set({ name: "accessToken", value: accessToken, - ...cookieOptions, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", maxAge: 60 * 60 * 2, }); @@ -53,15 +45,15 @@ export async function POST() { const parsed = cookie.parse(c); if (parsed.refreshToken) { - const maxAge = parsed["Max-Age"] - ? parseInt(parsed["Max-Age"], 10) - : 60 * 60 * 24 * 30; - - cookieHandler.set({ + res.cookies.set({ name: "refreshToken", value: parsed.refreshToken, - ...cookieOptions, - maxAge, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: parsed["Max-Age"] + ? parseInt(parsed["Max-Age"], 10) + : 60 * 60 * 24 * 30, }); } } diff --git a/src/app/my/page.tsx b/src/app/my/page.tsx index 4b11121d..b6f55c8f 100644 --- a/src/app/my/page.tsx +++ b/src/app/my/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import { HydrationBoundary, QueryClient, diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx index 356c0bf6..51e503b2 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -37,7 +37,6 @@ export const LoginForm = ({ method: "POST", body: JSON.stringify({ email, password }), noAuth: true, - useBaseUrl: false, }); setAccessToken(data.accessToken); diff --git a/src/shared/api/config.ts b/src/shared/api/config.ts new file mode 100644 index 00000000..07a51037 --- /dev/null +++ b/src/shared/api/config.ts @@ -0,0 +1,13 @@ +import { headers } from "next/headers"; + +export async function getBaseUrl() { + const h = await headers(); + const host = h.get("host"); + + const protocol = + host?.includes("localhost") || process.env.NODE_ENV === "development" + ? "http" + : "https"; + + return `${protocol}://${host}`; +} diff --git a/src/shared/api/fetcher.server.ts b/src/shared/api/fetcher.server.ts index a29359c4..f960a420 100644 --- a/src/shared/api/fetcher.server.ts +++ b/src/shared/api/fetcher.server.ts @@ -1,83 +1,49 @@ -import { cookies } from "next/headers"; -import { AuthorizationError, NotFoundError, ServerError } from "../error/error"; - -const BASE_URL = process.env.NEXT_PUBLIC_API_URL; +import { headers } from "next/headers"; +import { NotFoundError, ServerError, AuthorizationError } from "../error/error"; +import { getBaseUrl } from "./config"; export async function serverFetch( endpoint: string, - options: RequestInit & { noAuth?: boolean } = {}, + options: RequestInit, ): Promise { - const { headers, noAuth, ...restOptions } = options; - const cookieStore = await cookies(); - - const defaultHeaders: HeadersInit = { - "Content-Type": "application/json", - }; - - if (!noAuth) { - const accessToken = cookieStore.get("accessToken")?.value; + const { headers: extraHeaders = {}, ...restOptions } = options; - console.log("[serverFetch] AccessToken", accessToken); + const h = await headers(); + const cookie = h.get("cookie") ?? ""; - if (accessToken) { - defaultHeaders["Authorization"] = `Bearer ${accessToken}`; - } - } + const BASE_URL = await getBaseUrl(); + const url = `${BASE_URL}${endpoint}`; - let res = await fetch(`${BASE_URL}${endpoint}`, { - headers: { ...defaultHeaders, ...headers }, + const res = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...extraHeaders, + Cookie: cookie, + }, cache: "no-store", ...restOptions, }); - if (res.status === 401 && !noAuth) { - const refreshToken = cookieStore.get("refreshToken")?.value; - - if (!refreshToken) { - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", - ); - } - - const refreshRes = await fetch("/api/auth/refresh", { - method: "POST", - credentials: "include", - cache: "no-store", - }); - - console.log("[serverFetch] refreshRes status:", refreshRes.status); - - if (refreshRes.ok) { - const { accessToken: newToken } = await refreshRes.json(); - - res = await fetch(`${BASE_URL}${endpoint}`, { - headers: { - ...defaultHeaders, - ...headers, - Authorization: `Bearer ${newToken}`, - }, - credentials: "include", - cache: "no-store", - ...restOptions, - }); - } else { - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", - ); - } - } - - if (res.status === 404) { - throw new NotFoundError(); - } - - if (res.status >= 500) { - throw new ServerError(); + if (res.status === 401) { + throw new AuthorizationError( + "세션이 만료되었습니다.\n다시 로그인 해주세요.", + ); } if (!res.ok) { - const text = await res.text(); - throw new ServerError(text); + if (res.status === 404) throw new NotFoundError(); + if (res.status >= 500) throw new ServerError(); + + let message = `API Error ${res.status}`; + try { + const data = await res.json(); + message = data.message ?? data.detail ?? message; + } catch { + const text = await res.text(); + if (text) message = text; + } + + throw new ServerError(message); } return res.json() as Promise; diff --git a/src/shared/api/fetcher.ts b/src/shared/api/fetcher.ts index 1c54e836..b49d1907 100644 --- a/src/shared/api/fetcher.ts +++ b/src/shared/api/fetcher.ts @@ -1,74 +1,32 @@ -import { useAuthStore } from "@/features/auth/model/auth.store"; import { AuthorizationError, NotFoundError, ServerError } from "../error/error"; -const BASE_URL = process.env.NEXT_PUBLIC_API_URL; - -interface ApiFetchOptions extends RequestInit { - noAuth?: boolean; - useBaseUrl?: boolean; -} - export async function apiFetch( endpoint: string, - options: ApiFetchOptions = {}, + options: RequestInit & { noAuth?: boolean } = {}, ): Promise { - const { headers, noAuth, useBaseUrl = true, ...restOptions } = options; - - const { accessToken, setAccessToken, logout } = useAuthStore.getState(); - - const defaultHeaders: HeadersInit = { - "Content-Type": "application/json", - }; - - if (!noAuth && accessToken) { - defaultHeaders["Authorization"] = `Bearer ${accessToken}`; - } - - const finalUrl = useBaseUrl ? `${BASE_URL}${endpoint}` : endpoint; + const { noAuth, headers, ...restOptions } = options; - let res = await fetch(finalUrl, { - headers: { ...defaultHeaders, ...headers }, + const res = await fetch(endpoint, { + headers: { + "Content-Type": "application/json", + ...headers, + }, credentials: "include", cache: "no-store", ...restOptions, }); if (res.status === 401 && !noAuth) { - const refreshed = await fetch("/api/auth/refresh", { - method: "POST", - credentials: "include", - }); - - if (!refreshed.ok) { - logout(); - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", - ); - } - - const { accessToken: newToken } = await refreshed.json(); - - setAccessToken(newToken); - defaultHeaders["Authorization"] = `Bearer ${newToken}`; - - res = await fetch(finalUrl, { - headers: { ...defaultHeaders, ...headers }, - credentials: "include", - cache: "no-store", - ...restOptions, - }); + throw new AuthorizationError( + "세션이 만료되었습니다.\n다시 로그인 해주세요.", + ); } if (!res.ok) { - if (res.status === 404) { - throw new NotFoundError(); - } + if (res.status === 404) throw new NotFoundError(); + if (res.status >= 500) throw new ServerError(); - if (res.status >= 500) { - throw new ServerError(); - } let message = `API Error ${res.status}`; - try { const data = await res.json(); message = data.message ?? data.detail ?? message; diff --git a/src/widgets/header/ui/MobileSideMenu.tsx b/src/widgets/header/ui/MobileSideMenu.tsx index 1947532f..59a18f4c 100644 --- a/src/widgets/header/ui/MobileSideMenu.tsx +++ b/src/widgets/header/ui/MobileSideMenu.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useModalStore } from "@/shared/model/modal.store"; import { useAuthStore } from "@/features/auth/model/auth.store"; import cn from "@/shared/lib/cn"; import { apiFetch } from "@/shared/api/fetcher"; @@ -23,6 +24,7 @@ export default function MobileSideMenu({ }) { const router = useRouter(); const { isLogined, logout } = useAuthStore(); + const { openModal, closeModal } = useModalStore(); const [isVisible, setIsVisible] = useState(false); const queryClient = useQueryClient(); @@ -125,13 +127,29 @@ export default function MobileSideMenu({