Social Book & Mark
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev- create next project
cd sbm2025
pnpm dlx create-next-app@latest .- prettier & eslint setting
pnpm add -D prettier eslint-config-prettier pnpm add -D @trivago/prettier-plugin-sort-imports.prettierrc
{
"singleQuote": true,
"jsxSingleQuote": true,
"semi": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"parser": "typescript",
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": [
"^next$",
"^next/\\w*$",
"^next/(.*)$",
"^react$",
"^react/(.*)$",
"^lucide-react$",
"^@/lib/(.*)$",
"^[./]"
]
}.prettierignore 파일 생성해서 prettier에서 제외하기
# Markdown 파일 제외
*.md
*.css
# 이미지, JSON, log 제외
*.png
*.jpg
*.json
*.log
*.ico
*.svg
# node_modules와 빌드 폴더 제외
node_modules
dist
build
LICENSE
.*
*.sql
*.yaml
*.yml
*.json
*.prisma
package.json에 format script 걸기
"format": "prettier --write .",eslint.config.mjs 파일을 아래 내용으로 수정
import { FlatCompat } from '@eslint/eslintrc'
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
})
const eslintConfig = [
...compat.config({
extends: ['next', 'next/typescript', 'prettier'],
rules: {}
}),
]
export default eslintConfig- node setting
echo "v22" > .nvmrc- pnpm hoisting setting (for eslint)
echo "public-hoist-pattern[]=*eslint*" > .npmrc- 새롭게 pnpm 으로 설치
pnpm install- shadcn 설치
pnpm dlx shadcn@latest initpnpx shadcn@latest add dashboard-01- 테마(Dark/Light)
pnpm add next-themescomponents/theme-provider.tsx
'use client';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ComponentProps } from 'react';
export function ThemeProvider({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}app/layout.tsx
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
...
</ThemeProvider>components/theme-changer.tsx
import { useTheme } from 'next-themes';
const THEMES = ['light', 'system', 'dark'];
const { theme, setTheme } = useTheme();- next-auth
pnpm add next-auth@betalib/auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Kakao from 'next-auth/providers/kakao';
import Naver from 'next-auth/providers/naver';
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
session: {
strategy: 'jwt',
},
pages: {
// signIn: '/login',
},
providers: [Google, GitHub, Naver, Kakao],
});Auth key 생성 (.env.local에 자동으로 생성)
pnpm dlx auth secretdatabase & user 생성
mysql> create database sbmdb;
Query OK, 1 row affected (0.010 sec)
mysql> create user sbm@'%' identified by 'Book&Mark1';
Query OK, 0 rows affected (0.030 sec)
mysql> grant all privileges on sbmdb.* to sbm@'%';
Query OK, 0 rows affected (0.012 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.021 sec).env.local
DATABASE_URL="mysql://sbm@--@127.0.0.1:3309/sbmdb?connection_limit=5&pool_timeout=10"
AUTH_SECRET="FzF89ROlg5pOU/GBwHLxvJN"
AUTH_GOOGLE_ID="641566194982-20b29cu2ssn8.apps.googleusercontent.com"
AUTH_GOOGLE_SECRET=GOCSPX-pTpVkTlLswWi9X7
AUTH_GITHUB_ID=Ov23lilo1Te
AUTH_GITHUB_SECRET=3f776e98adfe551f474115d75ebf
AUTH_NAVER_ID="t7BZF1kwdh3"
AUTH_NAVER_SECRET="wMIch8ci"
AUTH_KAKAO_ID="d3134d7758c891199f62c9820"
AUTH_KAKAO_SECRET="7WoTBDPzI9MdtmfsyrqIlSTP"
app/api/auth/[...nextauth]/route.ts
export { GET, POST } from '@/lib/auth';
// export const runtime = 'edge'; // 'nodejs'app/layout.tsx 에 SessionProvider 걸기
import { SessionProvider } from 'next-auth/react';
import { auth } from '@/lib/auth';
const session = use(auth());
...
<SessionProvider session={session}>
...
</SessionProvider>middleware.ts 생성
import { NextResponse, type NextRequest } from 'next/server';
import { auth } from './lib/auth';
export async function middleware(req: NextRequest) {
const session = await auth();
const didLogin = !!session?.user;
if (!didLogin) {
const callbackUrl = encodeURIComponent(req.nextUrl.pathname);
return NextResponse.redirect(
new URL(`/api/auth/signin?callbackUrl=${callbackUrl}`, req.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|robots.txt|images|api/auth|login|regist|$).*)',
],
};- next.config.ts 에 이미지 domain 설정
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ hostname: '*.googleusercontent.com' },
{ hostname: 'avatars.githubusercontent.com' },
{ hostname: 'phinf.pstatic.net' },
{ hostname: '*.kakaocdn.net' },
],
},
};
export default nextConfig;- prisma setting
pnpm add -D prismadotenv-cli 설치하여 dotenv 명령 사용
pnpm add -D dotenv-clipnpm dlx prisma init --datasource-provider mysql
# ==> 이런 후 .env 지우고 .env.local에 연결정보 세팅eslint.config.mjs (to build project)
ignorePatterns: ['lib/generated/prisma/**'],
table 생성 (sqls/ddl.sql)
package.json에 script 설정
"scripts": {
"db:pull": "dotenv -e .env.local prisma db pull",
"db:gen": "dotenv -e .env.local prisma generate",
"db:push": "dotenv -e .env.local prisma db push",
"db:reset": "dotenv -e .env.local prisma migrate reset",
"db:seed": "dotenv -e .env.local prisma db seed"
},
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},create table Member (
id int unsigned not null auto_increment primary key,
createdAt timestamp not null default current_timestamp,
updatedAt timestamp not null default current_timestamp on update current_timestamp,
nickname varchar(31) not null,
email varchar(255) not null,
passwd varchar(128) null,
image varchar(255) null,
introduce varchar(1024) null,
UNIQUE KEY `uniq_Member_email` (`email`)
);pnpm db:pull
generate해서 prisma-client 설치
pnpm db:gen
# ==> prisma/schema.prisma 생성
prisma client (lib/db.ts)
import { PrismaClient } from '@/lib/generated/prisma/client';
const prisma = new PrismaClient();
export default prisma;seed data (prisma/seed.ts)
async function main() {
const sico = await prisma.member.upsert({
where: { email: 'indiflex.sico@gmail.com' },
update: {},
create: {
email: 'indiflex.sico@gmail.com',
nickname: 'sico',
Books: {
create: {
title: 'sico first book',
withdel: false,
Marks: {
create: {
url: 'https://naver.com',
title: 'Naver',
descript: 'seeding...',
},
},
},
},
},
});
const indiflex = await prisma.member.upsert({
where: { email: 'indiflex.corp@gmail.com' },
update: {},
create: {
email: 'indiflex.corp@gmail.com',
nickname: 'indiflex',
Books: {
create: [
{
title: 'indiflex first book',
withdel: false,
},
{
title: 'indiflex second book',
withdel: true,
},
],
},
},
});
console.log({ sico, indiflex });
}- password cryptor module
pnpm add bcryptjs import { compare, hash } from 'bcryptjs';
const encPasswd = await hash(passwd, 10);
const isValid = await compare(passwd, encPasswd);- zod
pnpm add zod- login & regist (feat. next-auth)
- auth.ts
- sign.ts (server action)
- app/login/page.tsx 작성