Jace Docs

monorepo.sh

monorepo 만드는 쉘스크립트

실행

> mkdir monorepo
> cd monorepo
> vi monorepo.sh
> chmod +x monorepo.sh
> ./monorepo.sh

monorepo.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}"

On this page