5월 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 과는 어떠한 연관 관련이 없음을 알려드립니다. 또한, 저작권에 관련된 문제있는 글이나 기타 저작권에 관련된 문제가 있는 것은 연락주시면 바로 삭제해 드리겠습니다.
댓글 ( 4)
댓글 남기기