middle
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '@/auth';
|
||||
import { PrismaClient } from '@/app/generated/prisma';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function parseDateOr(value: string | null, fallback: Date): Date {
|
||||
if (!value) return fallback;
|
||||
const d = new Date(value);
|
||||
return isNaN(d.getTime()) ? fallback : d;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const email = session.user?.email as string | undefined;
|
||||
if (!email) return NextResponse.json({ error: '세션 이메일을 찾을 수 없습니다' }, { status: 400 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const endDefault = new Date();
|
||||
const startDefault = new Date(endDefault.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const startParam = searchParams.get('start');
|
||||
const endParam = searchParams.get('end');
|
||||
const startDate = startParam ? new Date(startParam) : startDefault;
|
||||
const endDate = endParam ? new Date(endParam) : endDefault;
|
||||
// 날짜 경계 보정: 하루 전체 포함 (UTC 기준)
|
||||
const startInclusive = new Date(startDate);
|
||||
startInclusive.setUTCHours(0, 0, 0, 0);
|
||||
const endInclusive = new Date(endDate);
|
||||
endInclusive.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
// 안전가드
|
||||
if (endInclusive < startInclusive) endInclusive.setTime(startInclusive.getTime());
|
||||
console.log('startDate', startDate.toISOString());
|
||||
console.log('endDate', endDate.toISOString());
|
||||
|
||||
// 1) 내 핸들
|
||||
const handles = await prisma.userHandle.findMany({
|
||||
where: { email },
|
||||
select: { id: true, handle: true, icon: true }
|
||||
});
|
||||
if (handles.length === 0) return NextResponse.json({ items: [] });
|
||||
const handleStrs = handles.map(h => h.handle);
|
||||
const handleByStr = new Map(handles.map(h => [h.handle, h] as const));
|
||||
|
||||
// 2) 매핑된 콘텐츠
|
||||
const links = await prisma.contentHandle.findMany({
|
||||
where: { handle: { in: handleStrs } },
|
||||
select: { contentId: true, handle: true }
|
||||
});
|
||||
if (links.length === 0) return NextResponse.json({ items: [] });
|
||||
const contentIds = links.map(l => l.contentId);
|
||||
const normalizeId = (s: string) => (s ?? '').trim();
|
||||
const contentIdsNormalized = Array.from(new Set(contentIds.map(normalizeId)));
|
||||
const idsForQuery = Array.from(new Set([...contentIds, ...contentIdsNormalized]));
|
||||
const handleByContentId = new Map(links.map(l => [l.contentId, l.handle] as const));
|
||||
|
||||
// 3) 콘텐츠 본문
|
||||
const contents = await prisma.content.findMany({
|
||||
where: { id: { in: contentIds } },
|
||||
select: {
|
||||
id: true, subject: true, pubDate: true,
|
||||
views: true, premiumViews: true, watchTime: true
|
||||
}
|
||||
});
|
||||
|
||||
// 4) 기간 합계 유효조회수 (findMany로 가져와 JS에서 합산 - groupBy 일부 환경 이슈 회피)
|
||||
const viewRows = await prisma.viewPerDay.findMany({
|
||||
where: {
|
||||
contented: { in: idsForQuery },
|
||||
date: { gte: startInclusive, lte: endInclusive }
|
||||
},
|
||||
select: { contented: true, validViewDay: true }
|
||||
});
|
||||
const validSumById = new Map<string, number>();
|
||||
for (const r of viewRows) {
|
||||
const key = normalizeId(r.contented);
|
||||
validSumById.set(key, (validSumById.get(key) ?? 0) + (r.validViewDay ?? 0));
|
||||
}
|
||||
|
||||
// 5) 비용 단가
|
||||
const cpvRow = await prisma.costPerView.findUnique({ where: { id: 1 } });
|
||||
const cpv = cpvRow?.costPerView ?? 0;
|
||||
|
||||
const items = contents.map(c => {
|
||||
const handle = handleByContentId.get(c.id) ?? '';
|
||||
const handleObj = handleByStr.get(handle);
|
||||
const validViews = validSumById.get(normalizeId(c.id)) ?? 0;
|
||||
const expectedRevenue = validViews * cpv;
|
||||
return {
|
||||
id: c.id,
|
||||
subject: c.subject,
|
||||
pubDate: c.pubDate,
|
||||
views: c.views,
|
||||
premiumViews: c.premiumViews,
|
||||
watchTime: c.watchTime,
|
||||
handle,
|
||||
handleId: handleObj?.id ?? null,
|
||||
icon: handleObj?.icon ?? '',
|
||||
validViews,
|
||||
expectedRevenue,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ items, cpv, start: startInclusive.toISOString(), end: endInclusive.toISOString() });
|
||||
} catch (e) {
|
||||
console.error('my_contents 오류:', e);
|
||||
return NextResponse.json({ error: '조회 실패' }, { status: 500 });
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,10 @@ const NavBar: React.FC<NavBarProps> = ({ isOpen, setIsOpen, user }) => {
|
||||
<div className="flex-0 hidden lg:block">
|
||||
<Link href="/">
|
||||
<div className="flex items-center justify-center gap-1 mb-5 p-2 cursor-pointer">
|
||||
<svg width="32" height="20" viewBox="0 0 32 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<Image src="/falogo2.png" alt="logo" width={64} height={64} />
|
||||
{/* <svg width="32" height="20" viewBox="0 0 32 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 13.7815C0 11.1485 1.5051 9.48685 3.07055 8.61376C4.53165 7.7989 6.17151 7.56314 7.25262 7.56307H29.6288C30.9383 7.56309 32 8.65413 32 10C31.9999 11.3458 30.9383 12.4369 29.6288 12.4369H7.25262C6.70651 12.437 5.90554 12.5782 5.33295 12.8974C5.06963 13.0443 4.93117 13.1859 4.86171 13.2901C4.80809 13.3706 4.74246 13.5037 4.74246 13.7815C4.7425 14.0589 4.80813 14.1913 4.86171 14.2718C4.93111 14.376 5.06944 14.5175 5.33295 14.6644C5.90555 14.9838 6.70643 15.126 7.25262 15.1261H23.5259C24.8355 15.1261 25.8971 16.2172 25.8971 17.5631C25.8969 18.9088 24.8354 20 23.5259 20H7.25262C6.17151 19.9999 4.53165 19.763 3.07055 18.9481C1.50525 18.075 9.38884e-05 16.4143 0 13.7815ZM29.6288 0C30.9383 2.26081e-05 32 1.09107 32 2.43693C32 3.7828 30.9383 4.87385 29.6288 4.87387H14.5759C13.2664 4.87375 12.2046 3.78275 12.2046 2.43693C12.2046 1.09112 13.2664 0.000115545 14.5759 0H29.6288Z" fill="#F94B37" />
|
||||
</svg>
|
||||
</svg> */}
|
||||
<span className="text-black text-2xl font-bold">EVERFACTORY</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { signIn } from "next-auth/react"
|
||||
|
||||
export default function SignIn() {
|
||||
return <button onClick={() => signIn("google")}>
|
||||
return
|
||||
<button onClick={() => signIn("google")}>
|
||||
</button>
|
||||
}
|
||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import SignIn from "@/app/components/SignInClient";
|
||||
|
||||
export default function Test() {
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignIn/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export default function Home() {
|
||||
<Image src="/ever_logo.png" alt="logo" width={100} height={100} />
|
||||
</div>
|
||||
<div className="text-white text-4xl font-bold text-center mb-3">콘텐츠 음원 수익<br/>에버팩토리에서 편리하게</div>
|
||||
<div className="text-white text-md text-center mb-3">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lobortis maximus</div>
|
||||
{/* <div className="text-white text-md text-center mb-3">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi lobortis maximus</div> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-md text-center mb-3 cursor-pointer ">Ready to travel with us?<br/> Enter your email to create or restart your membership.</div>
|
||||
|
||||
@@ -5,7 +5,8 @@ import DashboardClient from "@/app/components/Dashboard"; // 기존 코드 대
|
||||
|
||||
export default async function Page() {
|
||||
const session = await auth();
|
||||
if (!session) redirect("/login");
|
||||
if (!session) redirect("/");
|
||||
|
||||
|
||||
// 초기 데이터 서버에서 미리 가져오면 더 빠르고 안전 (예시)
|
||||
// const [channels, contents] = await Promise.all([
|
||||
|
||||
12
auth.ts
12
auth.ts
@@ -11,5 +11,15 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
|
||||
],
|
||||
|
||||
// ...callbacks 등등
|
||||
events: {
|
||||
async signIn({ user, account, profile }) {
|
||||
console.log("[NextAuth] signIn user:", {
|
||||
id: (user as any)?.id,
|
||||
name: user?.name,
|
||||
email: user?.email,
|
||||
image: (user as any)?.image,
|
||||
provider: account?.provider,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -26,7 +26,6 @@ export default auth(async (req) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 이미 로그인된 경우 루트 경로로 접근 시 대시보드로 리다이렉트
|
||||
if (session) {
|
||||
if (pathname === "/") {
|
||||
|
||||
18
prisma/migrations/20250908172816_0908new/migration.sql
Normal file
18
prisma/migrations/20250908172816_0908new/migration.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `premiumViews` on the `content` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `validViews` on the `content` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `views` on the `content` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `watchTime` on the `content` table. All the data in the column will be lost.
|
||||
- You are about to drop the `costPerView` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `content` DROP COLUMN `premiumViews`,
|
||||
DROP COLUMN `validViews`,
|
||||
DROP COLUMN `views`,
|
||||
DROP COLUMN `watchTime`;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `costPerView`;
|
||||
49
prisma/migrations/20250908173757_0908new02/migration.sql
Normal file
49
prisma/migrations/20250908173757_0908new02/migration.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `ContentHandle` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `userHandle` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `handleId` to the `content` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `ContentHandle` DROP FOREIGN KEY `ContentHandle_contentId_fkey`;
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `ContentHandle` DROP FOREIGN KEY `ContentHandle_handle_fkey`;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `content` ADD COLUMN `handleId` VARCHAR(191) NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `ContentHandle`;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `userHandle`;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `user` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`icon` VARCHAR(191) NOT NULL DEFAULT '',
|
||||
`isApproved` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createtime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `handle` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`handle` VARCHAR(191) NOT NULL,
|
||||
`userId` VARCHAR(191) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `handle_handle_key`(`handle`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `content` ADD CONSTRAINT `content_handleId_fkey` FOREIGN KEY (`handleId`) REFERENCES `handle`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `handle` ADD CONSTRAINT `handle_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -14,16 +14,54 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model content{
|
||||
// user
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String
|
||||
icon String @default("")
|
||||
isApproved Boolean @default(false)
|
||||
createtime DateTime @default(now())
|
||||
RegisgerCode String @default("")
|
||||
|
||||
// Many-to-many: a user can have many handles
|
||||
handles Handle[]
|
||||
// One-to-many: a user can have many register channels
|
||||
registerChannels RegisterChannel[]
|
||||
}
|
||||
|
||||
model RegisterChannel {
|
||||
id String @id @default(uuid())
|
||||
handle String
|
||||
// Relation: many register channels belong to one user
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
// handle
|
||||
model Handle {
|
||||
id String @id @default(uuid())
|
||||
handle String @unique
|
||||
constPerView Float @default(1)
|
||||
// Relations
|
||||
contents Content[]
|
||||
// Many-to-many: a handle can belong to many users
|
||||
users User[]
|
||||
}
|
||||
|
||||
// content
|
||||
model Content{
|
||||
id String @id @default(uuid())
|
||||
subject String
|
||||
pubDate DateTime
|
||||
views Int
|
||||
validViews Int
|
||||
premiumViews Int
|
||||
watchTime Int
|
||||
// Back relation: a content may link to at most one handle
|
||||
contentHandle ContentHandle?
|
||||
// views Int
|
||||
// validViews Int
|
||||
// premiumViews Int
|
||||
// watchTime Int
|
||||
// Relation: many contents belong to one handle
|
||||
handleId String
|
||||
handle Handle @relation(fields: [handleId], references: [id])
|
||||
// Back relation: one content has many day views
|
||||
dayViews contentDayView[]
|
||||
}
|
||||
|
||||
model contentDayView{
|
||||
@@ -34,21 +72,13 @@ model contentDayView{
|
||||
validViews Int
|
||||
premiumViews Int
|
||||
watchTime Int
|
||||
// Relation: many day views belong to one content
|
||||
content Content @relation(fields: [contentId], references: [id])
|
||||
@@unique([contentId, date])
|
||||
}
|
||||
|
||||
|
||||
model userHandle {
|
||||
id String @id @default(uuid())
|
||||
email String
|
||||
handle String @unique
|
||||
icon String @default("")
|
||||
isApproved Boolean @default(false)
|
||||
createtime DateTime @default(now())
|
||||
// Relation: a handle can be linked to many contents
|
||||
contentLinks ContentHandle[]
|
||||
}
|
||||
|
||||
// notice
|
||||
model noticeBoard{
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
@@ -59,35 +89,6 @@ model noticeBoard{
|
||||
}
|
||||
|
||||
|
||||
model registerChannel {
|
||||
id String @id @default(uuid())
|
||||
email String
|
||||
handle String
|
||||
randomcode String
|
||||
createtime DateTime @default(now())
|
||||
}
|
||||
|
||||
model costPerView{
|
||||
id Int @id @default(1)
|
||||
costPerView Float
|
||||
}
|
||||
|
||||
|
||||
// Mapping table: Each content can be linked to at most one handle (unique contentId)
|
||||
// A handle can have many contents
|
||||
model ContentHandle {
|
||||
id String @id @default(uuid())
|
||||
contentId String @unique
|
||||
handle String
|
||||
|
||||
// Relations
|
||||
content content @relation(fields: [contentId], references: [id])
|
||||
user userHandle @relation(fields: [handle], references: [handle])
|
||||
|
||||
@@index([handle])
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
npx prisma migrate dev --name <migration_name>
|
||||
|
||||
BIN
public/falogo1.png
Normal file
BIN
public/falogo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 542 KiB |
BIN
public/falogo2.png
Normal file
BIN
public/falogo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
BIN
public/falogo3.png
Normal file
BIN
public/falogo3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
Reference in New Issue
Block a user