| 10월 2025 | ||||||
|---|---|---|---|---|---|---|
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 | |
src/auth.ts
// src/auth.ts
import NextAuth, { NextAuthConfig } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { CredentialsProviderError } from "./utils/auth/CredentialsProviderError";
import { supabase } from "@/lib/supabaseClient";
import { backendCredentialsProvider, monodbCredentialsProvider } from "./utils/auth/CredentialsProvider";
import { authEvents } from "./utils/auth/AuthEvents";
import authCallbacks from "./utils/auth/AuthCallbacks";
import { authConfig } from "./utils/auth/Auth.config";
import { OauthProviders } from "./utils/auth/OauthProvider";
const SERVER_TYPE = process.env.NEXT_SERVER_ACTIONS_TYPE || "mongo"; // 기본값: mongoose
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
unstable_update
} = NextAuth({
...authConfig,
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: {label: "username", type: "text", required: true},
password: {label: "password", type: "password", required: true },
},
async authorize(credentials: Partial<Record<"username"|"password" , unknown>>):Promise<any|null> {
if (!credentials || !credentials.username || !credentials.password) {
throw new CredentialsProviderError( "아이디와 비밀번호를 입력해주세요.", "username");
}
const username = credentials.username as string;
const password = credentials.password as string;
if(SERVER_TYPE=== "mongo"){//1.몽고 데이터베이스 연결
return await monodbCredentialsProvider({username, password});
}else if(SERVER_TYPE === "prisma"){//2. prisama 데이터베이스 연결
//user=await prisma.user.findUnique({where: { email:username }}) as UserType;
return null;
}else if(SERVER_TYPE === "supabase"){//3. supabase 데이터베이스 연결
const { data: supabaseUser} = await supabase.from("users").select("*").eq("username", username).single();
// user = supabaseUser;
return null;
}else if(SERVER_TYPE === "backend"){//4. backend 데이터베이스 연결
return await backendCredentialsProvider({username, password});
}
return null;
},
}),
...OauthProviders, // 분리한 소셜 로그인 provider 적용
],
callbacks: authCallbacks,
events: authEvents,
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error',
verifyRequest: '/auth/verify-request',
newUser: '/auth/signup',
},
//debug: process.env.NODE_ENV !== 'production',
} satisfies NextAuthConfig);
src/utils/auth/AuthCallbacks.ts
import { CustomSession } from "@/types/UserType";
import { cookies } from "next/headers";
import { postRequest } from "../AxiosInstance";
import { ResponseType } from "@/types/ResponseType";
import { saveTokenCookie } from "./AuthCookieSet";
import { decrypt } from "../crypto";
const SERVER_TYPE = process.env.NEXT_SERVER_ACTIONS_TYPE || "mongo"; // 기본값: mongoose
/**
* jwt token 세션을 거치지 않면 실질적으로 반환하는 함수
*/
const authCallbacks = (SERVER_TYPE === "backend" ) ? {
async signIn({ user, account, profile }: { user: any; account: any; profile?: any; }):
Promise<string | boolean>{
if (!account) {
console.error("OAuth 로그인 실패: account 객체가 존재하지 않습니다.");
return false;
}
if (['google', 'github', 'facebook', 'apple', 'kakao', 'naver'].includes(account?.provider || '')) {
const param = { provider: account?.provider, profile };
console.log("OAuth 로그인 후 백엔드에서 회원가입 처리 및 토큰 발행 처리:", param);
try {
const response =await postRequest<ResponseType>(`/api/backend/auth/oauth2`, param);
if (!response || !response.success) {
throw new Error(response.message || "OAuth 로그인 실패");
}
const data = response.data;
const customUser = user as unknown as CustomSession ;
customUser.provider=account.provider;
Object.assign(customUser, data); // 데이터를 커스텀 사용자 객체에 병합
return true;
} catch (error) {
console.error("OAuth 로그인 중 오류 발생:", error);
return false;
}
}
return true;
},
async jwt({ token, account, user} : { token : any, account: any, user: any }) {
const cookieInstance = await cookies();
if(account && user) {
const savedToken = {
...token,
provider:account.provider|| "local",
user:user.user
} as CustomSession;
//로그인시 토큰 및 유저데이터 쿠키에 저장
saveTokenCookie(cookieInstance, savedToken);
return savedToken;
}
//⭕쿠키에 저장된 토큰값을 불러와 반환 시킨다.⭕
const savedToken=cookieInstance.get("savedToken")?.value;
if(savedToken){
const accessTokenExpires=cookieInstance.get("accessTokenExpires")?.value;
if(accessTokenExpires && Date.now() < Number(accessTokenExpires) * 1000){
console.log("????접근토큰 남은시간(초)????:",
Math.floor(((Number(accessTokenExpires)*1000)-Date.now()) / 1000)+"초");
}
// ???? 토큰 값 복호화
return JSON.parse(decrypt(savedToken!));
}
return token;
},
async session({ session, token }: { session: any, token: any }) {
if (token) {
session={...token};
}
return session;
},
} : undefined;
export default authCallbacks;
/**
user: {
id: 1,
username: 'test1',
name: '홍길동7',
email: 'test1@gmail.com',
image: '/uploads/이미지.jpg',
roles: [ 'user' ],
accessToken: '11',
refreshToken: '22',
accessTokenExpires: 1742340624,
refreshTokenExpires: 1743550164
},
sub: 'fd63e95c-6341-4246-a4ae-53068d669ef4',
provider: 'local'
}
*/
src/utils/auth/AuthCookieSet.ts
import { CustomSession } from "@/types/UserType";
import { cookies } from "next/headers";
import { encrypt } from "../crypto";
export const saveTokenCookie = async(cookieInstance: any, savedToken:CustomSession) => {
await deleteTokenCookie(cookieInstance);
const secureSet = process.env.NODE_ENV === "production";
// ???? 토큰 값 암호화
cookieInstance.set("savedToken", encrypt(JSON.stringify(savedToken)), {
httpOnly: true,
secure: secureSet,
sameSite: "lax",
maxAge: savedToken.user.refreshTokenExpires, //✔️갱신토큰 만료시간으로 설정
path: "/",
});
cookieInstance.set("accessToken", encrypt(savedToken.user.accessToken), {
httpOnly: true,
secure: secureSet, // 운영 환경에서만 true
sameSite: "lax", // CSRF 공격 방어
maxAge: savedToken.user.accessTokenExpires,
path: "/",
});
cookieInstance.set("accessTokenExpires",savedToken.user?.accessTokenExpires?.toString(),{
httpOnly: true,
secure: secureSet,
sameSite: "lax",
maxAge: savedToken.user.accessTokenExpires,
path: "/",
});
cookieInstance.set("refreshToken", encrypt(savedToken.user?.refreshToken), {
httpOnly: true,
secure: secureSet,
sameSite: "lax",
maxAge: savedToken.user.refreshTokenExpires,
path: "/",
});
cookieInstance.set("refreshTokenExpires", savedToken.user?.refreshTokenExpires?.toString(), {
httpOnly: true,
secure: secureSet,
sameSite: "lax",
maxAge: savedToken.user.refreshTokenExpires,
path: "/",
});
console.log("********* 쿠키 저장 완료*********", savedToken);
};
export const deleteTokenCookie =async (cookieInstance: any) => {
if(!cookieInstance){
cookieInstance = await cookies();
}
cookieInstance.delete('savedToken');
cookieInstance.delete('accessToken');
cookieInstance.delete('accessTokenExpires');
cookieInstance.delete('refreshToken');
cookieInstance.delete('refreshTokenExpires');
};
src/utils/AxiosInstance.ts
import axios, { AxiosInstance } from "axios";
// ???? Axios 인스턴스 생성 함수
const createAxiosInstance = (isMultipart = false): AxiosInstance => {
return axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
timeout: 10000,
headers: isMultipart ? {} : { "Content-Type": "application/json" },
withCredentials: true,
});
};
// ✅ 일반 요청 및 멀티파트 요청용 Axios 인스턴스
export const axiosClient = createAxiosInstance();
export const axiosClientMultipart = createAxiosInstance(true);
const pendingRequests = new Map(); // 요청 추적을 위한 맵
const DEFAULT_DEBOUNCE_TIME = 1000; // 디바운스 시간(ms)
const DEFAULT_RETRY_COUNT = 3; // 기본 리트라이 횟수
const DEFAULT_RETRY_DELAY = 1000; // 기본 리트라이 딜레이(ms)
// ✅ 요청 고유 키 생성 함수 (디바운스 및 중복 방지)
const getRequestKey = (method: string, url: string, params: any = {}, data: any = {}) => {
return `${method.toUpperCase()}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
};
// ???? 지수 백오프 리트라이 함수 (재시도 로직)
const retryWithExponentialBackoff = async (fn: () => Promise<any>, retries = DEFAULT_RETRY_COUNT, delay = DEFAULT_RETRY_DELAY) => {
let attempt = 0;
while (attempt < retries) {
try {
return await fn();
} catch (error) {
attempt++;
console.warn(`⚠️ 요청 재시도 ${attempt}/${retries}`, error);
if (attempt >= retries) throw handleError(error);
await new Promise((resolve) => setTimeout(resolve, delay * Math.pow(2, attempt - 1)));
}
}
};
// ⏳ 디바운스 요청 처리 함수
const debounceRequest = async (
method: string,
url: string,
requestFn: () => Promise<any>,
debounceTime: number = DEFAULT_DEBOUNCE_TIME
) => {
const requestKey = getRequestKey(method, url);
if (debounceTime === 0 || !pendingRequests.has(requestKey)) {
const requestPromise = requestFn()
.catch(handleError)
.finally(() => {
if (debounceTime > 0) setTimeout(() => pendingRequests.delete(requestKey), debounceTime);
});
pendingRequests.set(requestKey, requestPromise);
return requestPromise;
}
return pendingRequests.get(requestKey);
};
// ???? 공통 에러 핸들러 함수
const handleError = (error: any) => {
if (axios.isAxiosError(error)) {
const errorResponseData = error.response?.data || {};
return {
status: error.response?.status || 500,
success: false,
message: errorResponseData.message || "알 수 없는 오류 발생",
error: errorResponseData.error || { code: "SERVER_ERROR", message: error.message },
};
}
return { status: 500, success: false, message: "UNKNOWN_ERROR", error: { code: "UNKNOWN", message: "알 수 없는 오류 발생" } };
};
// ⭕ GET 요청 (디바운스 & 리트라이 적용)
export const getRequest = async <T>(url: string, params?: object,config: object = {}): Promise<T> => {
return debounceRequest("get", url, () => retryWithExponentialBackoff(async () => {
const response = await axiosClient.get<T>(url, { params, ...config }); // ✅ config 추가
return response.data;
})).catch(handleError);
};
// ⭕ POST 요청 (디바운스 & 리트라이 적용)
export const postRequest = async <T>(url: string, data?: object | FormData, isFormData = false, config: object = {}): Promise<T> => {
return debounceRequest("post", url, () => retryWithExponentialBackoff(async () => {
const instance = isFormData ? axiosClientMultipart : axiosClient;
const response = await instance.post<T>(url, data, config);
return response.data;
})).catch(handleError);
};
// ⭕ PUT 요청 (디바운스 & 리트라이 적용)
export const putRequest = async <T>(url: string,data?: object | FormData,isFormData = false, config: object = {}): Promise<T> => {
return debounceRequest("put", url, () => retryWithExponentialBackoff(async () => {
const instance = isFormData ? axiosClientMultipart : axiosClient;
const response = await instance.put<T>(url, data, config);
return response.data;
})).catch(handleError);
};
// ⭕ PATCH 요청 (디바운스 & 리트라이 적용)
export const patchRequest=async <T>(url: string,data?: object | FormData,isFormData = false,config:object ={} ): Promise<T> => {
return debounceRequest("patch", url, () => retryWithExponentialBackoff(async () => {
const instance = isFormData ? axiosClientMultipart : axiosClient;
const response = await instance.patch<T>(url, data, config);
return response.data;
})).catch(handleError);
};
// ⭕ DELETE 요청 (디바운스 & 리트라이 적용)
export const deleteRequest =async<T>(url: string,config:object={}): Promise<T> => {
return debounceRequest("delete", url, () => retryWithExponentialBackoff(async () => {
const response = await axiosClient.delete<T>(url, config);
return response.data;
})).catch(handleError);
};
src/utils/AxiosServerToken.ts
import { ResponseType } from '@/types/ResponseType';
import axios, { AxiosInstance } from 'axios';
import { auth } from '@/auth';
import { CustomSession } from '@/types/UserType';
import { cookies } from 'next/headers';
import { postRequest } from './AxiosInstance';
import { deleteTokenCookie, saveTokenCookie } from './auth/AuthCookieSet';
import { decrypt } from './crypto';
// ⭕ 서버에서 사용할 Axios 인스턴스 생성⭕
const createAxiosInstance = (isMultipart = false): AxiosInstance => {
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL, // ✅ baseURL 추가
timeout: 10000,
headers: isMultipart ? {} : { "Content-Type": "application/json" },
withCredentials: true, // ✅ 쿠키 포함 설정
});
// 인증 인터셉터 추가
instance.interceptors.request.use(
async (config) => {
const session = await auth() as CustomSession;
if (session){ //인증이 필요한곳
const cookieInstance=await cookies();
const accessTokenExpires=cookieInstance.get("accessTokenExpires")?.value;
if(!accessTokenExpires || Date.now() >= Number(accessTokenExpires) * 1000){
console.log("???????????????????????? 만료 updateTokenGenerator 호출 :");
await refreshTokenGenerator(session as CustomSession, cookieInstance);
}
// ???? 토큰 값 복호화
const accessToken=decrypt(cookieInstance.get("accessToken")?.value);
//config.headers['Authorization'] = Bearer ${response.data.accessToken};
config.headers['Authorization']=process.env.NODE_ENV === "production" ? '' : `Bearer ${accessToken}`;
config.headers['credentials'] = "include";
}
return config;
},
(error) => Promise.reject(error)
);
return instance;
};
// Axios 인스턴스 생성
export const axiosServer = createAxiosInstance();
export const axiosServerMultipart = createAxiosInstance(true);
//⭕ 응답 인터셉터: 토큰 갱신 실패 및 인증 오류로 로그아웃처리
const refreshInterceptor = async (error: any) => {
const originalRequest = error.config;
if (error.response?.data?.message === "ACCESS_TOKEN_EXPIRED" && !originalRequest._retry) {
originalRequest._retry = true;
const session = await auth();
const cookieInstance = await cookies();
try {
if (!session || !cookieInstance.has("refreshToken")) throw new Error("No valid session or refreshToken");
await refreshTokenGenerator(session as CustomSession, cookieInstance);
if (originalRequest.headers["Content-Type"] === "multipart/form-data") {
return axiosServerMultipart(originalRequest);
} else {
return axiosServer(originalRequest);
}
} catch (refreshError) {
await deleteTokenCookie(cookieInstance);
console.error("???? 응답 인터셉터 오류 ", refreshError);
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
};
/**
* ⭕접근토큰 및 갱신 토큰 발급 처리 함수
*/
const refreshTokenGenerator = async ( session: CustomSession, cookieInstance : any) => {
console.log("???? 갱신 토큰 만료 update 함수 호출 :");
// ???? 토큰 값 복호화
const refreshToken = decrypt(cookieInstance.get("refreshToken")?.value);
const refreshTokenExpires =cookieInstance.get("refreshTokenExpires")?.value;
if ((refreshToken && isTokenExpired10Sec(refreshToken, 20) )&&
(refreshTokenExpires && Date.now() < Number(refreshTokenExpires) * 1000 )) {
const updateData = await postRequest<ResponseType>("/api/authToken/refreshToken", { refreshToken, session });
if(updateData && updateData.success) {
saveTokenCookie(cookieInstance, updateData.data);
return updateData;
}
}
return Promise.reject(new Error("Failed to refresh token"));
}
//접근 토큰 발급 경과 시간 확인
const isTokenExpired10Sec = (accessToken: string, timeout: number): boolean => {
//????accessToken 이 생성 된지 20초이상 경과 되었는지 확인
try {
const base64Url = accessToken.split('.')[1]; // JWT의 payload 부분 추출
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); // Base64 복원
const payload = JSON.parse(atob(base64)); // 디코딩 후 JSON 변환
const issuedAt = payload.iat; // 발급된 시간 (초 단위)
const currentTime = Math.floor(Date.now() / 1000); // 현재 시간 (초 단위)
return (currentTime - issuedAt) >= timeout; // 20초 이상 경과했는지 확인 true
} catch (error) {
console.error("JWT 파싱 실패:", error);
return true; // 에러 발생 시 기본적으로 만료된 것으로 처리
}
};
axiosServer.interceptors.response.use((res) => res, refreshInterceptor);
axiosServerMultipart.interceptors.response.use((res) => res, refreshInterceptor);
// ???? 공통 에러 핸들러 함수
const handleError = (error: any) => {
if (axios.isAxiosError(error)) {
const errorResponseData = error.response?.data || {};
return {
status: error.response?.status || 500,
success: false,
message: errorResponseData.message || "알 수 없는 오류 발생",
error: errorResponseData.error || { code: "SERVER_ERROR", message: error.message },
};
}
return { status: 500, success: false, message: "UNKNOWN_ERROR", error: { code: "UNKNOWN", message: "알 수 없는 오류 발생" } };
};
const pendingRequests = new Map(); // 요청 추적
const DEFAULT_DEBOUNCE_TIME = 1000; // 디바운스 시간(ms)
// ✅ 요청 고유 키 생성 함수 (디바운스 및 중복 방지)
const getRequestKey = (method: string, url: string, params: any = {}, data: any = {}) => {
return `${method.toUpperCase()}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
};
/**
* ⭕ 디바운스 요청 함수 ⭑
* 동일한 `requestKey`에 대한 요청이 일정 시간(`debounceTime`) 내에 연속적으로 호출되는 경우,
* 첫 번째 요청이 완료될 때까지 새로운 요청을 방지하여 불필요한 네트워크 요청을 줄임.
*
* @param requestKey - 요청을 식별하는 키 (예: API URL)
* @param requestFn - 실행할 비동기 요청 함수
* @param debounceTime - 디바운스 대기 시간(ms), 기본값은 `DEFAULT_DEBOUNCE_TIME`
* @returns 기존 요청이 진행 중이면 해당 Promise 반환, 새로운 요청이면 실행 후 Promise 반환
*/
// ⏳ 디바운스 요청 처리 함수
const debounceRequest = async (
method: string,
url: string,
requestFn: () => Promise<any>,
debounceTime: number = DEFAULT_DEBOUNCE_TIME
) => {
const requestKey = getRequestKey(method, url);
if (debounceTime === 0 || !pendingRequests.has(requestKey)) {
const requestPromise = requestFn()
.then((result) => {
setTimeout(() => pendingRequests.delete(requestKey), debounceTime);
return result;
})
.catch((error) => {
pendingRequests.delete(requestKey);
throw error;
});
pendingRequests.set(requestKey, requestPromise);
return requestPromise;
}
return pendingRequests.get(requestKey);
};
// ⭕ GET 요청 ⭕
/**
* 서버에서 GET 요청을 수행하고 디바운싱 적용
*
* @param url - 요청할 API 엔드포인트
* @param params - 요청 파라미터 (옵션)
* @param debounceTime - 디바운스 시간 (옵션)
* @returns 응답 데이터를 제네릭 타입 `T`로 반환
*/
export const getRequestTokenServer = async <T>(url: string, params?: object, debounceTime?: number): Promise<T> => {
return debounceRequest("get", url, async () => {
try {
const config = { params: params ?? {} }; // undefined 방지
const response = await axiosServer.get<T>(url, config);
return response.data;
} catch (error) {
throw handleError(error);
}
}, debounceTime);
};
// POST 요청
export const postRequestTokenServer = async <T>(url: string, data?: object | FormData,
isFormData = false, debounceTime?: number): Promise<T> => {
return debounceRequest("post", url, async () => {
try {
console.log("⭕⭕⭕POST request token server");
const instance = isFormData ? axiosServerMultipart : axiosServer;
const response = await instance.post<T>(url, data);
return response.data;
} catch (error) {
throw handleError(error);
}
}, debounceTime);
};
// PUT 요청
export const putRequestTokenServer = async <T>(url: string, data?: object | FormData, isFormData = false, debounceTime?: number): Promise<T> => {
return debounceRequest("put", url, async () => {
try {
const instance = isFormData ? axiosServerMultipart : axiosServer;
const response = await instance.put<T>(url, data);
return response.data;
} catch (error) {
throw handleError(error);
}
}, debounceTime);
};
// PATCH 요청
export const patchRequestTokenServer = async <T>(url: string, data?: object | FormData, isFormData = false, debounceTime?: number): Promise<T> => {
return debounceRequest("patch", url, async () => {
try {
const instance = isFormData ? axiosServerMultipart : axiosServer;
const response = await instance.patch<T>(url, data);
return response.data;
} catch (error) {
throw handleError(error);
}
}, debounceTime);
};
// DELETE 요청
export const deleteRequestTokenServer = async <T>(url: string, params?: object, debounceTime?: number): Promise<T> => {
return debounceRequest("delete", url, async () => {
try {
const config = { params: params ?? {} };
const response = await axiosServer.delete<T>(url, config);
return response.data;
} catch (error) {
throw handleError(error);
}
}, debounceTime);
};
src/app/api/authToken/refreshToken/route.ts
"use server";
import { NextResponse } from "next/server";
import { handleApiError } from "@/utils/HandleApiError";
import { CustomSession } from "@/types/UserType";
import { deleteTokenCookie } from "@/utils/auth/AuthCookieSet";
import { postRequest } from "@/utils/AxiosInstance";
import { ResponseType } from "@/types/ResponseType";
export async function GET(request: Request, response: Response){
return NextResponse.json({
status: 200,
success: true,
message: "/api/authToken/refreshToken",
});
}
/** /api/authToken/refreshToken
* ???? 갱신 토큰 가져오기
* @param request
* @returns
*/
export async function POST(request: Request) {
try {
//⭕일반적인 api 라우터 영역이 아니다. 따라서, axios instance 에서서 refreshToken, session 을 받아야 한다.
const { refreshToken, session } = await request.json();
if (!refreshToken || !session) throw new Error("refreshToken not found");
let headers ={};
if (process.env.NODE_ENV !== "production") headers= { headers: { Authorization: `Bearer ${refreshToken}` } }
const responseData = await postRequest<ResponseType>(`${process.env.NEXT_PUBLIC_API_BACKEND_BASE_URL}/api/auth/refresh`,
{}, // 요청 본문 없음
false, // FormData 여부 (JSON이므로 false)
headers // ✅ headers 추가
);
if (!responseData|| !responseData.success) throw new Error("Failed to refresh token");
const updateToken = { ...session, user: responseData.data.user } as CustomSession;
console.log("✅/api/authToken/refreshToken 성공 : ", updateToken);
return NextResponse.json({
success: true,
message: "토큰이 성공적으로 갱신되었습니다.",
data: updateToken,
});
} catch (error) {
console.log("????토큰 발급 에러????", error);
await deleteTokenCookie(false);
return handleApiError(error);
}
}
2025-03-19 19:32:26
{
"message": "Cannot GET /",
"error": "Not Found",
"statusCode": 404
}2025-02-19 03:30:12
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_aG9uZXN0LWhhbXN0ZXItMjUuY2xlcmsuYWNjb3VudHMuZGV2JA CLERK_SECRET_KEY=sk_test_cW305NU98TG0s8Dev8sgQSYVdBK56BstcEmt8yoGm5 NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/chat NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/chat DATABASE_URL="mysql://gptgenius:gptgenius1111@macaronics.iptime.org:16606/gptgenius" SHADOW_DATABASE_URL="mysql://gptgenius:gptgenius1111@macaronics.iptime.org:16606/prisma_shadow" UNSPLASH_API_ACCESS_KEY=j1GLKfSKKDknnok2AFJGSpOuSGy8I1H0GeTwwhkWHN8 UNSPLASH_API_SECRET_KEY=lG8uAUv0XiKfqW4mK5ofiYQtfhYvjjoZNzCZJeQWQBY
2025-01-30 21:41:51
macaronics.net 는 그어떠한 동영상, 이미지, 파일등을 직접적으로 업로드 제공을 하지 않습니다. 페이스북, 트위터 등 각종 SNS 처럼 macaronics.net 는 웹서핑을 통하여 각종 페이지위치등을 하이퍼링크, 다이렉트링크, 직접링크등으로 링크된 페이지 주소만을 수집 저장하여 제공하고 있습니다. 저장된 각각의 주소에 연결된 페이지등은 그 페이지에서 제공하는 "서버, 사이트" 상황에 따라 페이지와 내용이 삭제 중단 될 수 있으며 macaronics.net 과는 어떠한 연관 관련이 없음을 알려드립니다. 또한, 저작권에 관련된 문제있는 글이나 기타 저작권에 관련된 문제가 있는 것은 연락주시면 바로 삭제해 드리겠습니다.
댓글 ( 0)
댓글 남기기