monorepo.sh
monorepo 만드는 쉘스크립트
실행
> mkdir monorepo
> cd monorepo
> vi monorepo.sh
> chmod +x monorepo.sh
> ./monorepo.shmonorepo.sh
#!/bin/bash
# 색상 및 스타일
GREEN='\033[0;32m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m'
echo -e "${PURPLE}====================================================${NC}"
echo -e "${PURPLE} JACE ULID-Driven Monorepo Scaffolding (2026) ${NC}"
echo -e "${PURPLE}====================================================${NC}"
# 1. 루트 초기화
echo -e "${BLUE}📦 [1/6] 루트 워크스페이스 및 .gitignore 설정 중...${NC}"
mkdir -p apps packages
pnpm init
cat <<EOF >.gitignore
node_modules/
.pnpm-store/
.turbo
dist/
build/
.react-router/
.cache/
.env
*.env.local
.DS_Store
EOF
cat <<EOF >pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
EOF
pnpm add -D turbo typescript tsx vite-tsconfig-paths -w
cat <<EOF >turbo.json
{
"\$schema": "https://turbo.build/schema.json",
"globalPassThroughEnv": [
"DATABASE_URL",
"DIRECT_URL",
"BETTER_AUTH_SECRET",
"BETTER_AUTH_URL",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"KAKAO_CLIENT_ID",
"KAKAO_CLIENT_SECRET",
"NAVER_CLIENT_ID",
"NAVER_CLIENT_SECRET"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".react-router/**", "build/**"]
},
"db:generate": { "cache": false },
"db:push": { "cache": false },
"dev": { "cache": false, "persistent": true }
}
}
EOF
# 2. packages/database (ULID Default 적용)
echo -e "${BLUE}🗄️ [2/6] DB 패키지 구축 (@gnss/db)...${NC}"
mkdir -p packages/database/src/common
cd packages/database
pnpm init
sed -i '' 's/"name": "database"/"name": "@gnss\/db"/' package.json
# ulidx를 DB 패키지에도 추가하여 스키마에서 직접 사용
pnpm add drizzle-orm postgres ulidx
pnpm add -D drizzle-kit
cat <<EOF >drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/common/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: { url: process.env.DIRECT_URL! },
});
EOF
cat <<EOF >src/common/schema.ts
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
import { ulid } from "ulidx";
// 1. User 테이블
export const user = pgTable("user", {
id: text("id").primaryKey().\$defaultFn(() => ulid()),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
isPremium: boolean("is_premium").default(false).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'date' }).defaultNow().notNull(),
});
// 2. Session 테이블
export const session = pgTable("session", {
id: text("id").primaryKey().\$defaultFn(() => ulid()),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id").notNull().references(() => user.id),
});
// 3. Account 테이블 (소셜 연동)
export const account = pgTable("account", {
id: text("id").primaryKey().\$defaultFn(() => ulid()),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id").notNull().references(() => user.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
// 4. Verification 테이블
export const verification = pgTable("verification", {
id: text("id").primaryKey().\$defaultFn(() => ulid()),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at"),
updatedAt: timestamp("updated_at"),
});
EOF
cat <<EOF >src/common/relations.ts
import { relations } from "drizzle-orm";
import { user, session, account } from "./schema";
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { fields: [session.userId], references: [user.id] }),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { fields: [account.userId], references: [user.id] }),
}));
EOF
cat <<EOF >src/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as commonSchema from "./common/schema";
import * as commonRelations from "./common/relations";
export const schema = { ...commonSchema, ...commonRelations };
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is missing");
const client = postgres(process.env.DATABASE_URL!, { max: 1 });
export const db = drizzle(client, { schema });
export * from "drizzle-orm";
export * from "./common/schema";
export * from "./common/relations";
EOF
cd ../..
# 3. packages/auth 구축
echo -e "${BLUE}🔐 [3/6] Auth 패키지 구축 (@gnss/auth)...${NC}"
mkdir -p packages/auth/src
cd packages/auth
pnpm init
sed -i '' 's/"name": "auth"/"name": "@gnss\/auth"/' package.json
pnpm add better-auth ulidx @gnss/db@workspace:*
cat <<EOF >src/index.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db, schema } from "@gnss/db";
import { ulid } from "ulidx";
export const getAuth = (env: any) => {
return betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: schema,
generateId: () => ulid(),
}),
socialProviders: {
google: {
prompt: "select_account",
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
kakao: { clientId: env.KAKAO_CLIENT_ID, clientSecret: env.KAKAO_CLIENT_SECRET },
naver: { clientId: env.NAVER_CLIENT_ID, clientSecret: env.NAVER_CLIENT_SECRET },
},
account: { accountLinking: { enabled: true } },
emailAndPassword: {
enabled: true,
password: { hashOptions: { logN: 10, r: 8, p: 1 } }
},
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
session: {
expiresIn: 60 * 60 * 24 * 7,
cookieCache: { enabled: true, strategy: "jwt" },
},
cookie: {
domain: process.env.NODE_ENV === "production" ? ".gnss.com" : ".local.com",
},
trustedOrigins: [
"http://web.local.com:3000",
"http://admin.local.com:3001",
"https://calen.thejace.workers.dev",
],
});
};
EOF
cd ../..
# 4. Apps 생성 (Web, Admin)
echo -e "${BLUE}🌐 [4/6] 서비스 앱 초기화 중...${NC}"
for app in web admin; do
mkdir -p apps/$app/app/routes
cd apps/$app
pnpm init
sed -i '' "s/\"name\": \"$app\"/\"name\": \"@gnss\/$app\"/" package.json
pnpm add @gnss/db@workspace:* @gnss/auth@workspace:* better-auth
PORT=$([ "$app" == "web" ] && echo "3000" || echo "3001")
cat <<EOF >vite.config.ts
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
envDir: "../../",
server: { port: $PORT, host: "$app.local.com" },
plugins: [reactRouter(), tsconfigPaths()],
});
EOF
cd ../..
done
# 5. 환경 변수 및 마무리
echo -e "${BLUE}📝 [5/6] 환경 변수 설정...${NC}"
cat <<EOF >.env
DATABASE_URL="postgres://user:pass@host:6543/db?pgbouncer=true"
DIRECT_URL="postgres://user:pass@host:5432/db"
BETTER_AUTH_SECRET="$(openssl rand -hex 32)"
BETTER_AUTH_URL="http://web.local.com:3000"
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
KAKAO_CLIENT_ID=""
KAKAO_CLIENT_SECRET=""
NAVER_CLIENT_ID=""
NAVER_CLIENT_SECRET=""
EOF
# 6. 최종 설치
echo -e "${BLUE}🚚 [6/6] 최종 의존성 설치 중...${NC}"
pnpm install
echo -e "${GREEN}====================================================${NC}"
echo -e "${GREEN}✅ ULID 스키마가 포함된 설정이 완료되었습니다!${NC}"
echo -e "${BLUE}Mac Studio에서 pnpm dev로 바로 시작하세요.${NC}"
echo -e "${GREEN}====================================================${NC}"