Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
.github
.idea
.next
.env
.env.*
!.env.example
node_modules
eslint.config.mjs
prettier.config.mjs
.gitignore
README.md
tsconfig.tsbuildinfo
12 changes: 7 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
DATABASE_URL=postgres://user:password@url:port/schema
PAYLOAD_SECRET=PugM4PevIH
DATABASE_URL=postgres://user:password@host:5432/schema
S3_BUCKET=stdev-kr
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY=example
AWS_SECRET_KEY=example
SMTP_HOST=smtp.example.com
SMTP_USER=example
SMTP_PASS=example
BETTER_AUTH_SECRET=replace-with-generated-secret
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=example.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=example

NEXT_PUBLIC_CHANNEL_PLUGIN_KEY=example
NEXT_PUBLIC_GTM_ID=GTM-example
Expand Down
26 changes: 24 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ name: CD

on:
push:
branches: ["main"]
branches: ['main']
pull_request:
branches: ["main"]
branches: ['main']

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
NODE_VERSION: '24'

jobs:
build-and-push:
Expand All @@ -22,6 +23,20 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false

- name: Install Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
Expand Down Expand Up @@ -54,3 +69,10 @@ jobs:
"NEXT_PUBLIC_CHANNEL_PLUGIN_KEY=${{ secrets.NEXT_PUBLIC_CHANNEL_PLUGIN_KEY }}"
"NEXT_PUBLIC_GTM_ID=${{ secrets.NEXT_PUBLIC_GTM_ID }}"
"NEXT_PUBLIC_GA_ID=${{ secrets.NEXT_PUBLIC_GA_ID }}"
"DATABASE_URL=${{ secrets.DATABASE_URL }}"
"S3_BUCKET=${{ secrets.S3_BUCKET }}"
"AWS_REGION=${{ secrets.AWS_REGION }}"
"BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }}"
"BETTER_AUTH_URL=${{ secrets.BETTER_AUTH_URL }}"
"GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}"
"GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}"
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [main]

env:
NODE_VERSION: "24"
NODE_VERSION: '24'

jobs:
build:
Expand All @@ -24,7 +24,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache: 'pnpm'

- name: Copy example env file
run: cp .env.example .env
Expand All @@ -47,7 +47,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down
71 changes: 71 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# PROJECT KNOWLEDGE BASE

## OVERVIEW

STDev Corp. (사단법인 에스티데브) Korean nonprofit homepage. Next.js 16 App Router + React 19 + Prisma/Postgres DIY CMS + better-auth + Chakra UI v3, shipped as a standalone Docker image.

## STRUCTURE

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify the fenced code block language.

The fence at Line 9 has no language token and will keep MD040 warnings active.

Suggested patch
-```
+```text
 stdev/
 ├── prisma/schema.prisma      # CMS/auth data model
 ...
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 9, The Markdown fenced code block in AGENTS.md (the block
showing the directory tree starting with "stdev/") is missing a language token;
update the opening fence from ``` to include a token (for example ```text) so
the block is explicitly marked and MD040 warnings are silenced; locate the
fenced block around the "stdev/" directory listing and change the opening fence
accordingly.

stdev/
├── prisma/schema.prisma # CMS/auth data model
├── prisma.config.ts # Prisma config; reads DATABASE_URL
├── src/
│ ├── app/ # (stdev) public site, (cms) admin, api/auth
│ ├── components/ # UI building blocks
│ └── utils/ # cms.ts, prisma.ts, auth.ts, menus/links/date helpers
├── public/images/ # intro/, business/, gov/ static assets
└── .github/workflows/ # ci.yml (build+lint), cd.yml (ghcr.io image)
```

## WHERE TO LOOK

| Task | Location | Notes |
| ----------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Change CMS schema | `prisma/schema.prisma` | Then run `pnpm db:generate` and create/apply a migration |
| Public CMS query | `src/utils/cms.ts` | Server-only helpers used by async pages |
| Prisma client | `src/utils/prisma.ts` | Reuses one client during dev hot reload |
| Auth config | `src/utils/auth.ts` + `src/utils/admin-auth.ts` + `src/app/api/auth/[...all]/route.ts` | Google-only better-auth with Prisma adapter |
| Admin UI | `src/app/(cms)/admin/**` | DIY CMS forms protected by better-auth |
| Add a public page/route | `src/app/(stdev)/<path>/page.tsx` | Also update `src/utils/menus.ts` and `src/utils/links.ts` |
| Shared layout chrome | `src/components/layout/` | `basic-layout`, `left-menu-layout`, `navbar`, `footer` |
| Markdown rendering | `src/components/markdown/markdown-view.tsx` | Chakra-mapped react-markdown + remark-gfm |

## CONVENTIONS

- **Package manager**: pnpm@10.33.3 only. Never commit a non-pnpm lockfile.
- **Prettier**: `semi: false`, `singleQuote: true`, `trailingComma: 'all'`, `tabWidth: 2`.
- **Import alias**: `@/*` → `./src/*`.
- **Server vs client**: Pages are async server components by default. Client components declare `'use client'`.
- **Rendering**: `(stdev)/layout.tsx` sets `export const dynamic = 'force-dynamic'` for request-time CMS reads.
- **Locale**: `<html lang="ko">` + Korean UI strings; dates format as `YYYY년 M월 D일`.
- **Styling**: Public site uses Chakra v3 primitives. Admin currently uses simple React/HTML forms.
- **Env enforcement**: Required public envs are thrown on missing in layout/providers.

## ANTI-PATTERNS

- Do not edit generated Prisma client output in `node_modules`; change `prisma/schema.prisma` and regenerate.
- Do not add semicolons.
- Do not statically render `(stdev)`.
- Do not add new image remote hosts without updating `next.config.ts` `images.remotePatterns`.

## COMMANDS

```bash
pnpm dev # Next.js dev server on :3000
pnpm build # Production build
pnpm start # Run production build
pnpm lint # ESLint + prettier integration
pnpm prettier:write # Format all
pnpm prettier:check # CI-style check
pnpm db:generate # Generate Prisma client
pnpm db:migrate # Local schema migration
pnpm db:migrate:deploy # Production migration deploy
```

## NOTES

- No test suite exists. CI runs build + lint.
- Docker prod port is 1000.
- Existing S3 object URLs are preserved as CMS asset URLs; remote host is `stdev-kr.s3.ap-northeast-2.amazonaws.com`.
- `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and `DATABASE_URL` are required for the CMS/auth stack.
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ COPY . .
RUN --mount=type=secret,id=NEXT_PUBLIC_CHANNEL_PLUGIN_KEY,env=NEXT_PUBLIC_CHANNEL_PLUGIN_KEY \
--mount=type=secret,id=NEXT_PUBLIC_GTM_ID,env=NEXT_PUBLIC_GTM_ID \
--mount=type=secret,id=NEXT_PUBLIC_GA_ID,env=NEXT_PUBLIC_GA_ID \
--mount=type=secret,id=DATABASE_URL,env=DATABASE_URL \
--mount=type=secret,id=S3_BUCKET,env=S3_BUCKET \
--mount=type=secret,id=AWS_REGION,env=AWS_REGION \
--mount=type=secret,id=BETTER_AUTH_SECRET,env=BETTER_AUTH_SECRET \
--mount=type=secret,id=BETTER_AUTH_URL,env=BETTER_AUTH_URL \
--mount=type=secret,id=GOOGLE_CLIENT_ID,env=GOOGLE_CLIENT_ID \
--mount=type=secret,id=GOOGLE_CLIENT_SECRET,env=GOOGLE_CLIENT_SECRET \
corepack enable pnpm && pnpm run build

# Stage 3: Production server
Expand Down
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-
`.env.example` 파일을 참고하여 `.env` 파일을 작성합니다.

```bash
npm install
pnpm install
```

```bash
npm run dev
pnpm dev
```

브라우저에서 [http://localhost:3000](http://localhost:3000)로 이동합니다.
Expand All @@ -26,6 +26,11 @@ Github 레포지토리 설정에서 `Actions secrets and variables` 페이지로
- NEXT_PUBLIC_CHANNEL_PLUGIN_KEY=example
- NEXT_PUBLIC_GTM_ID=GTM-example
- NEXT_PUBLIC_GA_ID=G-example
- DATABASE_URL=postgres://user:password@url:port/schema
- BETTER_AUTH_SECRET=example
- BETTER_AUTH_URL=https://www.stdev.kr
- GOOGLE_CLIENT_ID=example.apps.googleusercontent.com
- GOOGLE_CLIENT_SECRET=example

서버에서 `Docker Compose` 환경을 설정한 후, `docker-compose.yml` 을 아래와 같이 작성합니다.

Expand All @@ -40,15 +45,23 @@ services:
restart: always
environment:
DATABASE_URL: postgres://user:password@url:port/schema
PAYLOAD_SECRET: example
BETTER_AUTH_SECRET: example
BETTER_AUTH_URL: https://www.stdev.kr
GOOGLE_CLIENT_ID: example.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET: example
AWS_REGION: ap-northeast-2
AWS_ACCESS_KEY: example
AWS_SECRET_KEY: example
SMTP_HOST: smtp.example.com
SMTP_USER: example
SMTP_PASS: example
S3_BUCKET: stdev-kr
```

아래 명령을 실행합니다.
컨테이너를 올리기 전에 Prisma 스키마 마이그레이션을 먼저 적용합니다.

```bash
pnpm db:migrate:deploy
```

그 다음 아래 명령을 실행합니다.

```bash
docker compose up -d
Expand Down
4 changes: 3 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import eslintConfigNext from 'eslint-config-next'
import eslintConfigPrettier from 'eslint-config-prettier'

const config = [
{
ignores: ['coverage/**', 'playwright-report/**', 'test-results/**'],
},
...eslintConfigNext,
eslintConfigPrettier,
{ ignores: ['./src/generated/payload-types.ts'] },
{
rules: {
'@typescript-eslint/no-unused-expressions': 'off',
Expand Down
20 changes: 13 additions & 7 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import type { NextConfig } from 'next'
import { withPayload } from '@payloadcms/next/withPayload'

const s3Hosts = new Set([
'stdev-kr.s3.ap-northeast-2.amazonaws.com',
process.env.S3_BUCKET
? `${process.env.S3_BUCKET}.s3.${process.env.AWS_REGION ?? 'ap-northeast-2'}.amazonaws.com`
: null,
])

const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
remotePatterns: [...s3Hosts]
.filter((hostname): hostname is string => Boolean(hostname))
.map((hostname) => ({
protocol: 'https',
hostname: 'stdev-kr.s3.ap-northeast-2.amazonaws.com',
hostname,
port: '',
pathname: '**',
search: '',
},
],
})),
},
experimental: {
authInterrupts: true,
},
}

export default withPayload(nextConfig)
export default nextConfig
52 changes: 28 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,56 @@
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"build": "prisma generate && next build",
"start": "next start",
"lint": "eslint",
"prettier:write": "prettier --write .",
"prettier:check": "prettier --check .",
"generate:types": "payload generate:types",
"generate:importmap": "payload generate:importmap"
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:studio": "prisma studio"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.984.0",
"@chakra-ui/react": "^3.32.0",
"@aws-sdk/client-s3": "^3.1042.0",
"@chakra-ui/react": "^3.35.0",
"@channel.io/channel-web-sdk-loader": "^2.0.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@next/third-parties": "16.1.6",
"@payloadcms/db-postgres": "^3.75.0",
"@payloadcms/email-nodemailer": "^3.75.0",
"@payloadcms/next": "^3.75.0",
"@payloadcms/richtext-lexical": "^3.75.0",
"@payloadcms/storage-s3": "^3.75.0",
"dayjs": "^1.11.19",
"graphql": "^16.12.0",
"next": "16.1.6",
"payload": "^3.75.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"@next/third-parties": "16.2.4",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"better-auth": "^1.6.9",
"dayjs": "^1.11.20",
"dotenv": "^17.4.2",
"next": "16.2.4",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.5"
"sharp": "^0.34.5",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^24.10.11",
"@types/react": "^19.2.13",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.6",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.4",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"prisma": "^7.8.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"@prisma/client",
"@prisma/engines",
"esbuild",
"prisma",
"sharp"
]
},
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
"packageManager": "pnpm@10.33.3+sha512.a19744364a7e248b92657a4ca5973f9354d21caf982579674b1c539f32c7420c47138ad8b1254df07aba9bc782d9b3029e3db34d5dbff974326eb74dac8ff489"
}
Loading
Loading