first commit

This commit is contained in:
2025-09-07 22:57:43 +00:00
commit 3bd542adbf
122 changed files with 45056 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
// app/dashboard/page.tsx (Server Component)
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import DashboardClient from "@/app/components/Dashboard"; // 기존 코드 대부분 이동
export default async function Page() {
const session = await auth();
if (!session) redirect("/login");
// 초기 데이터 서버에서 미리 가져오면 더 빠르고 안전 (예시)
// const [channels, contents] = await Promise.all([
// fetch(`${process.env.API_URL}/channels`, { cache: "no-store" }).then(r=>r.json()),
// fetch(`${process.env.API_URL}/contents`, { cache: "no-store" }).then(r=>r.json()),
// ]);
return (
<DashboardClient
// initialChannels={channels}
// initialContents={contents}
user={session.user} // id/email/name 등
/>
);
}

View File

@@ -0,0 +1,236 @@
"use client";
import CalenderSelector from "@/app/components/CalenderSelector";
import Modal from "@/app/components/Modal";
import { useEffect, useRef, useState } from "react";
export default function Page() {
const [isreqmodal, setIsreqmodal] = useState(false);
const [chanellist, setChanellist] = useState<string[]>([]);
const [contentlist, setContentlist] = useState<{
id: string;
email: string;
pubDate: string;
is_approved: boolean;
is_certified: boolean;
video_count: number;
views: number;
revenue: number;
pendingRevenue: number;
}[]>([]);
const [channelList, setChannelList] = useState<{
id: string;
handle: string;
createtime: string;
is_approved: boolean;
icon: string;
}[]>([]);
const [registerCode, setRegisterCode] = useState<string>("");
const handleInputRef = useRef<HTMLInputElement>(null);
const formatNumberWithCommas = (number: number): string => {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const get_code = async () => {
if (handleInputRef.current && handleInputRef.current.value) {
const inputValue = handleInputRef.current.value;
if (confirm(`핸들 "${inputValue}" 의 등록 코드를 발급하시겠습니까?`)) {
try {
const response = await fetch(`/api/channel_code?handle=${inputValue}`);
if (response.status === 409) {
alert("이미 등록된 요청입니다. 기존 코드를 사용합니다.");
const data = await response.json();
setRegisterCode(data.randomcode);
return;
}
if (!response.ok) {
throw new Error('서버 응답이 올바르지 않습니다.');
}
const data = await response.json();
setRegisterCode(data.randomcode);
} catch (error) {
console.error('채널 코드 요청 중 오류 발생:', error);
alert('채널 코드를 가져오는데 실패했습니다. 다시 시도해주세요.');
return;
}
return;
}
} else{ alert("채널 핸들을 입력해주세요"); }
}
const register_channel = async () => {
if (handleInputRef.current && handleInputRef.current.value) {
const inputValue = handleInputRef.current.value;
try {
const response = await fetch(`/api/register_channel?handle=${encodeURIComponent(inputValue)}`);
const data = await response.json();
if (!response.ok) {
alert(data?.error ?? '등록 요청 실패');
return;
}
alert('등록 요청을 전송했습니다.');
setIsreqmodal(false);
} catch (error) {
console.error('등록 요청 중 오류:', error);
alert('등록 요청 실패');
}
} else { alert("채널 핸들을 입력해주세요"); }
}
const fetchListChannel = async () => {
try {
const resp = await fetch('/api/list_channel', { cache: 'no-store' });
const data = await resp.json();
setChannelList(data.items);
console.log('list_channel:', data);
} catch (e) {
console.error('list_channel 요청 에러:', e);
}
}
useEffect(() => {
// 세션 이메일 기준 채널 목록 조회 로그
fetchListChannel();
}, []);
return (
<div className="flex flex-col w-[100vw] p-2 lg:w-[calc(100vw-350px)] max-w-[1200px] mx-auto">
<div className="flex flex-row h-[64px] justify-between items-center">
<div className="flex flex-row h-[36px]">
</div>
<div className="flex flex-row h-[36px]">
<div
className="w-[104px] h-[36px] rounded-lg flex items-center justify-center bg-[#F94B37] border-[#D73B29] text-white cursor-pointer hover:bg-[#D73B29]"
onClick={() => {setIsreqmodal(true); setRegisterCode("")}}
>
</div>
</div>
</div>
<div className="
border-1 border-[#e6e9ef] rounded-lg bg-white overflow-y-auto
">
<table className="w-full h-full border-separate border-spacing-y-1 table-fixed px-[10px]">
<colgroup>
<col className="min-w-[250px] max-w-[300px]"/>
<col className="w-[120px]"/>
<col className="w-[80px]"/>
<col className="w-[100px]"/>
<col className="w-[100px]"/>
<col className="w-[110px]"/>
</colgroup>
<thead>
<tr className="sticky top-0 bg-white h-[49px] ">
<th className="border-b-1 right-border border-[#e6e9ef] "> </th>
<th className="border-b-1 right-border border-[#e6e9ef] "> </th>
<th className="border-b-1 right-border border-[#e6e9ef] "></th>
<th className="border-b-1 right-border border-[#e6e9ef] "></th>
<th className="border-b-1 right-border border-[#e6e9ef] "></th>
<th className="border-b-1 border-[#e6e9ef] "></th>
</tr>
</thead>
<tbody>
{
channelList.map((channel)=>{
return (
<tr key={channel.id} className="h-[54px] border-1 border-[#e6e9ef] rounded-lg font-semibold">
<td className="right-border rounded-l-lg border-l-1 border-t-1 border-b-1 border-[#e6e9ef] pl-2 h-[54px] " >
<div className="flex flex-row items-center gap-2">
<div className="w-[48px] h-[48px] rounded-full border-1 border-[#e6e9ef]">
<img src={channel.icon} alt="channel icon" className="w-[48px] h-[48px] rounded-full" />
</div>
<div>
{channel.handle}
</div>
</div>
</td>
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{channel.createtime.split("T")[0]}</td>
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center whitespace-nowrap overflow-hidden">{channel.is_approved? "승인" : "미승인"}</td>
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
<td className="right-border border-b-1 border-t-1 border-[#e6e9ef] text-center"> - </td>
<td className="border-b-1 border-t-1 border-[#e6e9ef] border-r-1 rounded-r-lg text-center"> - </td>
</tr>
)
})}
</tbody>
</table>
</div>
<Modal isOpen={isreqmodal} onClose={() => setIsreqmodal(false)}>
<div className=" w-full h-full flex flex-col justify-start gap-5">
<div className="flex flex-row justify-between items-center">
<div className="text-3xl font-semibold"></div>
<div className="text-3xl cursor-pointer" onClick={() => setIsreqmodal(false)}>
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.951569 0.451569C1.4202 -0.0170597 2.18 -0.0170597 2.64863 0.451569L9.0001 6.80304L15.3516 0.451569C15.8202 -0.0170597 16.58 -0.0170597 17.0486 0.451569C17.5173 0.920199 17.5173 1.68 17.0486 2.14863L10.6972 8.5001L17.0486 14.8516C17.5173 15.3202 17.5173 16.08 17.0486 16.5486C16.58 17.0173 15.8202 17.0173 15.3516 16.5486L9.0001 10.1972L2.64863 16.5486C2.18 17.0173 1.4202 17.0173 0.951569 16.5486C0.48294 16.08 0.48294 15.3202 0.951569 14.8516L7.30304 8.5001L0.951569 2.14863C0.48294 1.68 0.48294 0.920199 0.951569 0.451569Z" fill="#848484" />
</svg>
</div>
</div>
<div className="flex flex-col sh-[287px]">
<div className="flex flex-col justify-between">
<div className="text-lg font-semibold text-black"></div>
<div className="flex flex-row justify-between items-center">
<div className="w-[400px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-center text-md p-1 px-2">
<input type="text" className="w-full h-full border-none outline-none" ref={handleInputRef}/>
</div>
<div className="ml-2 w-[140px] h-[50px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => get_code()}>
</div>
</div>
</div>
<div className="flex flex-col justify-between mt-5">
<div className="text-lg font-semibold text-black"> </div>
<div className="flex flex-row items-center gap-2">
<div className="w-full h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-start text-xl p-5">
{registerCode ? registerCode : "코드를 발급해주세요"}
</div>
{registerCode && (
<div
className="w-[45px] h-[56px] border-[#d5d5d5] border-1 bg-white rounded-lg flex items-center justify-center cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(registerCode);
}}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 9H11C9.89543 9 9 9.89543 9 11V20C9 21.1046 9.89543 22 11 22H20C21.1046 22 22 21.1046 22 20V11C22 9.89543 21.1046 9 20 9Z" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M5 15H4C3.46957 15 2.96086 14.7893 2.58579 14.4142C2.21071 14.0391 2 13.5304 2 13V4C2 3.46957 2.21071 2.96086 2.58579 2.58579C2.96086 2.21071 3.46957 2 4 2H13C13.5304 2 14.0391 2.21071 14.4142 2.58579C14.7893 2.96086 15 3.46957 15 4V5" stroke="#666666" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
)}
</div>
<div className="mt-3 w-[100%-50px] h-[56px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer hover:bg-[#D73B29] transition-colors" onClick={() => register_channel()}>
</div>
</div>
</div>
</div>
</Modal>
</div>
);
}

127
app/usr/3_jsmanage/page.tsx Normal file
View File

@@ -0,0 +1,127 @@
'use client'
import Modal from "@/app/components/Modal";
import { useEffect, useState } from "react";
export default function Page() {
const [noticeList, setNoticeList] = useState<{
id: string;
title: string;
is_approved: boolean;
createdAt: Date;
}[]>([]);
const [isreqmodal, setIsreqmodal] = useState(false);
const [isAdmin, setIsAdmin] = useState(true);
useEffect(() => {
setNoticeList([
{
id: '1',
title: '8월 정산 요청',
is_approved: false,
createdAt: new Date(),
},
{
id: '2',
title: '7월 정산 요청',
is_approved: false,
createdAt: new Date(),
},
{
id: '3',
title: '6월 정산 요청',
is_approved: true,
createdAt: new Date(),
},
]);
}, []);
return (
<div className="bg-white h-full">
{isAdmin && (
<div className="w-full flex flex-row justify-end items-center px-4 max-w-[800px] mx-auto">
<div
className="w-[86px] h-[36px] border-1 border-[#d5d5d5] bg-[#f94b37] text-white font-semibold rounded-lg flex items-center justify-center my-4 hover:bg-[#d73b29] cursor-pointer"
onClick={() => setIsreqmodal(true)}
>
</div>
</div>
)}
<div className="h-[calc(100%-80px)] min-w-[500px] max-w-[800px] mx-auto overflow-y-auto p-4 flex flex-col">
{noticeList.map((notice) => (
<div key={notice.id} className="w-full h-[75px] border-[#d5d5d5] border-1 rounded-lg p-4 my-2 flex flex-row justify-between hover:border-[#F94B37] cursor-pointer">
<div className="flex flex-col justify-between">
<div className="flex flex-row justify-start ">
<div className="mr-2 flex items-center justify-center">
{!notice.is_approved && ( <div className="w-[40px] h-[20px] bg-[#FEDBD7] text-[#F94B37] font-semibold text-xs rounded-md text-center flex items-center justify-center"></div>
)}
{notice.is_approved && ( <div className="w-[50px] h-[20px] bg-[#d5d5d5] text-black font-semibold text-xs rounded-md text-center flex items-center justify-center"></div>
)}
</div>
<div className="text-normal font-semibold flex justify-center items-center truncate overflow-hidden"> {notice.title} </div>
</div>
<div className="text-xs text-[#848484]">
{notice.createdAt.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })}
</div>
</div>
<div className="flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 15L12.5 10L7.5 5" stroke="#848484" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
))}
</div>
<Modal isOpen={isreqmodal} onClose={() => setIsreqmodal(false)}>
<div className=" w-full h-full flex flex-col justify-between">
<div className="flex flex-row justify-between items-center">
<div className="text-3xl font-semibold"></div>
<div className="text-3xl cursor-pointer" onClick={() => setIsreqmodal(false)}>
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.951569 0.451569C1.4202 -0.0170597 2.18 -0.0170597 2.64863 0.451569L9.0001 6.80304L15.3516 0.451569C15.8202 -0.0170597 16.58 -0.0170597 17.0486 0.451569C17.5173 0.920199 17.5173 1.68 17.0486 2.14863L10.6972 8.5001L17.0486 14.8516C17.5173 15.3202 17.5173 16.08 17.0486 16.5486C16.58 17.0173 15.8202 17.0173 15.3516 16.5486L9.0001 10.1972L2.64863 16.5486C2.18 17.0173 1.4202 17.0173 0.951569 16.5486C0.48294 16.08 0.48294 15.3202 0.951569 14.8516L7.30304 8.5001L0.951569 2.14863C0.48294 1.68 0.48294 0.920199 0.951569 0.451569Z" fill="#848484" />
</svg>
</div>
</div>
<div className="flex flex-col justify-between h-[287px]">
<div className="flex flex-col justify-between">
<div className="text-lg font-semibold text-black"> </div>
<div className="w-[428px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-bold rounded-lg flex items-center justify-center text-xl">
000,000,000
</div>
</div>
<div className="flex flex-col justify-between">
<div className="text-lg font-semibold text-black"> </div>
<div className="w-[428px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-bold rounded-lg flex items-center justify-center text-xl">
000,000,000
</div>
</div>
<div className="flex flex-col justify-between">
<div className="text-lg font-semibold text-black"> </div>
<div className="w-[428px] h-[56px] border-[#d5d5d5] border-1 bg-white text-black font-normal rounded-lg flex items-center justify-starttext-xl p-5">
@ 3
</div>
</div>
</div>
<div>
<div className="w-[428px] h-[56px] border-[#D73B29] bg-[#F94B37] text-white font-bold rounded-lg flex items-center justify-center text-xl cursor-pointer" onClick={() => setIsreqmodal(false)}>
</div>
</div>
</div>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,141 @@
'use client'
import { useEffect, useState } from "react";
import NoticeModal from "@/app/components/ModelNotice";
import { MilkdownViewer } from "@/app/components/MilkdownViewer";
import { MilkdownProvider } from "@milkdown/react";
import { Crepe } from "@milkdown/crepe";
import { useRef } from "react";
export default function Page() {
const crepeRef = useRef<Crepe>(null!);
const [noticeList, setNoticeList] = useState<{
id: string;
title: string;
pubDate: Date;
content: string;
tag: string;
}[]>([]);
const [currentNotice, setCurrentNotice] = useState<{
id: string;
title: string;
content: string;
tag:string
pubDate: Date;
}>({
id: "",
tag: "",
title: "",
content: "",
pubDate: new Date("0"),
});
const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
const openHandler = (id: string) => {
const notice = noticeList.find((notice) => notice.id === id);
if (notice) {
setCurrentNotice({
id: notice.id,
title: notice.title,
content: notice.content,
tag: notice.tag,
pubDate: notice.pubDate,
});
}
setIsNoticeModalOpen(true);
}
const fetchNotices = async () => {
const response = await fetch('/api/notice');
const data = await response.json();
setNoticeList(data);
console.log(data);
}
useEffect(() => {
fetchNotices();
}, []);
return (
<div className="bg-white h-full w-[100vw] lg:w-[calc(100vw-360px)]">
<div className="h-[calc(100%-80px)] max-w-[800px] mx-auto overflow-y-auto p-4 flex flex-col">
{noticeList.length === 0 ? (
<div className="text-center text-gray-500"> .</div>
) : (
noticeList.map((notice) => (
<div
key={notice.id}
className="h-[75px] border-[#d5d5d5] border-1 rounded-lg p-4 my-2 flex flex-row justify-between hover:border-[#F94B37] cursor-pointer min-w-[100px] shrink-0 basis-0 grow-0"
onClick={() => openHandler(notice.id)}
>
{/* 여기 min-w-0 추가해서 자식을 제한함 부모와 이 위치 관계 다시 확인 */}
<div className="flex flex-col justify-between min-w-0">
<div className="flex flex-row justify-start">
{
notice.tag === "중요" && (
<div className="mr-2 flex items-center justify-center">
<div className="w-[33px] h-[20px] bg-[#FEDBD7] text-[#F94B37] font-semibold text-xs rounded-lg text-center flex items-center justify-center"></div>
</div>
)
}
<div className="text-normal font-semibold truncate">
{notice.title}
</div>
</div>
<div className="text-xs text-[#848484]">
{notice.pubDate.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })}
</div>
</div>
<div className="flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 15L12.5 10L7.5 5" stroke="#848484" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
))
)}
</div>
<NoticeModal isOpen={isNoticeModalOpen} onClose={() => setIsNoticeModalOpen(false)}>
<div className="flex flex-col h-full bg-white rounded-lg p-6">
<div className="flex justify-end mb-4">
<button
className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
onClick={() => setIsNoticeModalOpen(false)}
>
</button>
</div>
<div className="flex flex-col border-b border-gray-200 pb-4 mb-4">
<h1 className="text-2xl font-bold text-gray-800">{currentNotice.title}</h1>
<p className="text-sm text-gray-400 mt-1">
{currentNotice.pubDate.toLocaleString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})}
</p>
</div>
<div className="flex-1 overflow-y-auto">
<MilkdownProvider>
<MilkdownViewer value={currentNotice.content} editorRef={crepeRef} />
</MilkdownProvider>
</div>
</div>
</NoticeModal>
</div>
);
}

0
app/usr/5_admin/page.tsx Normal file
View File

13
app/usr/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
// app/layout.tsx (Server Component)
import { auth } from "@/auth";
import LayoutClient from "@/app/components/LayoutClient";
export default async function Layout({ children }: { children: React.ReactNode }) {
const session = await auth();
return (
<LayoutClient session={session}>
{children}
</LayoutClient>
);
}

8
app/usr/page.tsx Normal file
View File

@@ -0,0 +1,8 @@
export default function InitPage() {
return (
<div>
<h1>Initialization Page</h1>
<p>Welcome to the initialization page. Please follow the instructions to set up your application.</p>
</div>
);
}

12
app/usr/test/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client";
import { useSession } from "next-auth/react";
export default function WhoAmI() {
const { data } = useSession();
return (
<div>
{data?.user?.name} / {data?.user?.email}
<br />id: {(data?.user as any)?.id}
</div>
);
}