Merge branch 'main' into deploy
Deploy Frontend / deploy (push) Successful in 10m24s

This commit is contained in:
2026-07-24 16:59:38 +09:00
40 changed files with 468 additions and 514 deletions
+15 -5
View File
@@ -19,11 +19,21 @@
}
],
"paths": {
"@/*": ["./src/*"],
"@common/components": ["../../packages/common/components/dist/"],
"@common/network": ["../../packages/common/network/"]
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
-1
View File
@@ -32,7 +32,6 @@
"next": "15.5.18",
"react-hook-form": "^7.54.2",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"zod": "^3.24.2"
},
"devDependencies": {
@@ -1,7 +1,11 @@
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
import { type AuthPayload, type AuthService } from "@common/auth";
import {
type AuthPayload,
type AuthService,
clearDeviceId,
getOrCreateDeviceId,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
async function login(payload: AuthPayload): Promise<void> {
@@ -1,17 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
+22 -4
View File
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,9 +23,23 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
-1
View File
@@ -47,7 +47,6 @@
"sonner": "^2.0.1",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
@@ -5,18 +5,17 @@ import {
ENDPOINT,
} from "@/shared/config";
import {
clearDeviceId,
getOrCreateDeviceId,
type AuthPayload,
type AuthService,
getRemainingMs,
} from "@common/auth";
import {
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
} from "../lib/token-manager";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
function completeSession(accessToken: string, tokenId: string): void {
setToken(CLAIM_NAME, accessToken);
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
+16 -10
View File
@@ -3,7 +3,11 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,21 +27,23 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
+10 -19
View File
@@ -22,26 +22,17 @@
"name": "next"
}
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@common/assets": ["../../../../packages/common/assets/dist/"],
"@common/components": ["../../../../packages/common/components/dist/"],
"@common/network": ["../../../../packages/common/network/"]
"@/*": ["./src/*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
-1
View File
@@ -45,7 +45,6 @@
"sonner": "^2.0.1",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
@@ -4,9 +4,13 @@ import {
CLAIM_NAME,
ENDPOINT,
} from "@/shared/config";
import { type AuthPayload, type AuthService } from "@common/auth";
import {
type AuthPayload,
type AuthService,
clearDeviceId,
getOrCreateDeviceId,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
setToken,
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
+16 -10
View File
@@ -3,7 +3,11 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,21 +27,23 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
+1 -2
View File
@@ -35,8 +35,7 @@
"react-hook-form": "^7.54.2",
"zod": "^3.24.2",
"axios": "^1.8.1",
"proj4": "^2.9.0",
"uuid": "^14.0.1"
"proj4": "^2.9.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+79 -79
View File
@@ -1,47 +1,82 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
} from "@/src/shared/config";
import { getCookie } from "@/src/shared/lib";
import {
type AuthPayload,
type AuthService,
getRemainingMs,
clearDeviceId,
getOrCreateDeviceId,
type AuthPayload,
type AuthService,
getRemainingMs,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
} from "../lib/token-manager";
import { clearTabStorePersist } from "@/src/store/tab-store";
async function login(payload: AuthPayload): Promise<void> {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
@@ -49,67 +84,32 @@ async function login(payload: AuthPayload): Promise<void> {
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"X-Device-Id": getOrCreateDeviceId(),
},
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
}
export async function logout(): Promise<void> {
tokenManager.clearAuthToken();
clearDeviceId();
tokenManager.clearAuthToken();
clearDeviceId();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
};
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
-1
View File
@@ -37,7 +37,6 @@
"react-hook-form": "^7.54.2",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"xlsx": "^0.18.5",
"xlsx-populate": "^1.21.0",
"zod": "^3.24.2",
+79 -78
View File
@@ -1,47 +1,83 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
} from "@/shared/config";
import { getCookie } from "@/shared/lib";
import {
type AuthPayload,
type AuthService,
getRemainingMs,
clearDeviceId,
getOrCreateDeviceId,
type AuthPayload,
type AuthService,
getRemainingMs,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
} from "../lib/token-manager";
import { clearTabStorePersist } from "../../../store/tab-store";
async function login(payload: AuthPayload): Promise<void> {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
@@ -49,67 +85,32 @@ async function login(payload: AuthPayload): Promise<void> {
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"X-Device-Id": getOrCreateDeviceId(),
},
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
}
export async function logout(): Promise<void> {
tokenManager.clearAuthToken();
clearDeviceId();
tokenManager.clearAuthToken();
clearDeviceId();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
};
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
-1
View File
@@ -57,7 +57,6 @@
"swr": "^2.3.7",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^5.0.3"
@@ -5,9 +5,13 @@ import {
ENDPOINT,
} from "@/shared/config";
import { getCookie } from "@/shared/lib";
import { type AuthPayload, type AuthService } from "@common/auth";
import {
type AuthPayload,
type AuthService,
clearDeviceId,
getOrCreateDeviceId,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
setToken,
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
+46 -40
View File
@@ -1,43 +1,49 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"exclude": ["**/node_modules", "**/.*/*"]
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
]
}
-1
View File
@@ -49,7 +49,6 @@
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"uuid": "^14.0.1",
"xlsx": "^0.18.5",
"xlsx-populate": "^1.21.0",
"zod": "^3.24.2",
+79 -78
View File
@@ -1,47 +1,83 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
ENDPOINT,
} from "@/src/shared/config";
import { getCookie } from "@/src/shared/lib";
import {
type AuthPayload,
type AuthService,
getRemainingMs,
clearDeviceId,
getOrCreateDeviceId,
type AuthPayload,
type AuthService,
getRemainingMs,
} from "@common/auth";
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
import {
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
clearRefreshTimer,
setToken,
setupRefreshTimer,
tokenManager,
} from "../lib/token-manager";
import { clearTabStorePersist } from "@/src/store/tab-store";
async function login(payload: AuthPayload): Promise<void> {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Device-Id": getOrCreateDeviceId(),
"X-Device-Id": getOrCreateDeviceId(),
},
body: JSON.stringify(payload),
});
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("인증 토큰을 받지 못했습니다.");
}
const prevTokenId = getCookie(AUTH_USER_ID);
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
clearTabStorePersist();
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
@@ -49,67 +85,32 @@ async function login(payload: AuthPayload): Promise<void> {
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
setupRefreshTimer(silentRefresh, remainingMs);
}
}
async function silentRefresh(): Promise<void> {
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"X-Device-Id": getOrCreateDeviceId(),
},
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
}
setToken(CLAIM_NAME, accessToken);
setToken(AUTH_USER_ID, tokenId);
const remainingMs = getRemainingMs(accessToken);
if (remainingMs > 0) {
setupRefreshTimer(silentRefresh, remainingMs);
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
} catch (error) {
console.error(
`[silentRefresh] Failed at ${new Date().toISOString()}`,
error,
);
tokenManager.clearAuthToken();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
}
export async function logout(): Promise<void> {
tokenManager.clearAuthToken();
clearDeviceId();
tokenManager.clearAuthToken();
clearDeviceId();
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("auth:logout"));
}
}
export const authService: AuthService = {
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
login,
silentRefresh,
declineSessionExtend: clearRefreshTimer,
logout,
};
@@ -1,20 +0,0 @@
import { v4 as uuidv4 } from "uuid";
const DEVICE_ID_KEY = "x-device-id";
export function getOrCreateDeviceId(): string {
if (typeof window === "undefined") return "";
const existing = localStorage.getItem(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
localStorage.setItem(DEVICE_ID_KEY, id);
return id;
}
export function clearDeviceId(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(DEVICE_ID_KEY);
}
+7 -1
View File
@@ -1,6 +1,12 @@
{
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"exclude": [
"dist",
"build",
"node_modules",
"coverage",
"storybook-static"
],
"compilerOptions": {
"moduleResolution": "node",
"baseUrl": "."
+1
View File
@@ -19,6 +19,7 @@ packages/common/auth/
├── types.ts # 타입 정의
├── token-manager.ts # 토큰 관리 로직
├── cookie.ts # 쿠키 유틸리티
├── device-id.ts # 기기 ID (쿠키)
├── jwt-utils.ts # JWT 유틸리티
└── README.md # 문서
```
+27
View File
@@ -0,0 +1,27 @@
import { v4 as uuidv4 } from "uuid";
import { getCookie, removeCookie, setCookie } from "./cookie";
const DEVICE_ID_KEY = "x-device-id";
const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
export function getOrCreateDeviceId(): string {
const existing = getCookie(DEVICE_ID_KEY);
if (existing) return existing;
const id = uuidv4();
const isHttps =
typeof window !== "undefined" && window.location.protocol === "https:";
setCookie(DEVICE_ID_KEY, id, {
secure: isHttps,
sameSite: "lax",
domain: COOKIE_DOMAIN,
path: "/",
expires: 365,
});
return id;
}
export function clearDeviceId(): void {
removeCookie(DEVICE_ID_KEY, { path: "/" });
}
+1
View File
@@ -2,6 +2,7 @@ export { createTokenManager } from "./token-manager";
export type { TokenManager } from "./token-manager";
export { getCookie, setCookie, removeCookie } from "./cookie";
export { clearDeviceId, getOrCreateDeviceId } from "./device-id";
export {
getJwtExpiry,
getRemainingMs,
+2 -1
View File
@@ -8,7 +8,8 @@
".": "./index.ts"
},
"dependencies": {
"js-cookie": "^3.0.5"
"js-cookie": "^3.0.5",
"uuid": "^14.0.1"
},
"devDependencies": {
"@types/js-cookie": "^3.0.6",
+7 -1
View File
@@ -3,6 +3,7 @@
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"types": ["node"],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@@ -19,5 +20,10 @@
"incremental": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": [
"node_modules",
"dist",
"coverage",
"storybook-static"
]
}
+2
View File
@@ -1,2 +1,4 @@
export * from "./api";
export * from "./config";
export * from "./lib";
export * from "./ui";
+9 -8
View File
@@ -12,21 +12,22 @@
"noEmit": false,
"incremental": true,
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
}
},
"exclude": ["**/node_modules", "**/.*/*", "dist"],
"exclude": [
"node_modules",
".next",
"dist",
"coverage",
"storybook-static",
"public"
],
"include": [
".eslintrc.*",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".storybook/env.ts"
]
}
+6 -1
View File
@@ -9,5 +9,10 @@
"incremental": false
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules",
"dist",
"coverage",
"storybook-static"
]
}
+4
View File
@@ -9,6 +9,8 @@ interface ClientConfig {
siteId: string;
}
const DEVICE_ID_KEY = "x-device-id";
class Client {
private static instance: Client;
private axiosInstance: AxiosInstance;
@@ -26,6 +28,7 @@ class Client {
this.axiosInstance.interceptors.request.use((config) => {
const token = Cookies.get(CLAIM_NAME);
const userId = Cookies.get(AUTH_USER_ID);
const deviceId = Cookies.get(DEVICE_ID_KEY);
if (!token || !userId) {
return config;
@@ -33,6 +36,7 @@ class Client {
config.headers[CLAIM_NAME] = token;
config.headers[AUTH_USER_ID] = userId;
config.headers["X-Device-Id"] = deviceId;
return config;
});
+8 -3
View File
@@ -2,9 +2,14 @@
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext"
"module": "ESNext",
"types": ["node"]
},
"include": ["."],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules",
"dist",
"coverage",
"storybook-static"
]
}
+3 -21
View File
@@ -195,9 +195,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
zod:
specifier: ^3.24.2
version: 3.25.76
@@ -352,9 +349,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
zod:
specifier: ^3.24.2
version: 3.25.76
@@ -639,9 +633,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
zod:
specifier: ^3.24.2
version: 3.25.76
@@ -951,9 +942,6 @@ importers:
select:
specifier: ^1.1.2
version: 1.1.2
uuid:
specifier: ^14.0.1
version: 14.0.1
webpack:
specifier: ^5.98.0
version: 5.105.4
@@ -1078,9 +1066,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
xlsx:
specifier: ^0.18.5
version: 0.18.5
@@ -1280,9 +1265,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
xlsx:
specifier: ^0.18.5
version: 0.18.5
@@ -1440,9 +1422,6 @@ importers:
tus-js-client:
specifier: ^4.3.1
version: 4.3.1
uuid:
specifier: ^14.0.1
version: 14.0.1
xlsx:
specifier: ^0.18.5
version: 0.18.5
@@ -1495,6 +1474,9 @@ importers:
js-cookie:
specifier: ^3.0.5
version: 3.0.5
uuid:
specifier: ^14.0.1
version: 14.0.1
devDependencies:
'@types/js-cookie':
specifier: ^3.0.6
+8 -1
View File
@@ -18,5 +18,12 @@
"incremental": true,
"newLine": "lf"
},
"exclude": ["**/node_modules", "**/.*/"]
"exclude": [
"**/node_modules",
"**/.*/",
"**/dist",
"**/coverage",
"**/storybook-static",
"**/.next"
]
}