Skip to content
Closed

test #89

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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

# Optional: salt for server-side view fingerprint hashing
VIEW_FINGERPRINT_SALT=replace-with-random-secret

# Umami Analytics (self-hosted)
NEXT_PUBLIC_UMAMI_URL=https://your-umami.vercel.app
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-website-id
Expand Down
164 changes: 79 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,101 +1,95 @@
<div align="center">

# eunu.log

[![Next.js](https://img.shields.io/badge/Next.js-16+-black?style=flat-square&logo=next.js)](https://nextjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)

개인 블로그입니다.

[라이브 데모](https://eunu-log.vercel.app)

</div>

## 🛠 기술 스택

<table>
<tr>
<td align="center" width="96">
<img src="https://skillicons.dev/icons?i=nextjs" width="48" height="48" alt="Next.js" />
<br>Next.js
</td>
<td align="center" width="96">
<img src="https://skillicons.dev/icons?i=react" width="48" height="48" alt="React" />
<br>React
</td>
<td align="center" width="96">
<img src="https://skillicons.dev/icons?i=ts" width="48" height="48" alt="TypeScript" />
<br>TypeScript
</td>
<td align="center" width="96">
<img src="https://skillicons.dev/icons?i=threejs" width="48" height="48" alt="Three.js" />
<br>Three.js
</td>
<td align="center" width="96">
<img src="https://skillicons.dev/icons?i=tailwind" width="48" height="48" alt="CSS" />
<br>Tailwind
</td>
</tr>
</table>
`eunu.log`는 장문 기술 글과 실무 회고를 다루는 Next.js 기반 개인 블로그다.
MDX 콘텐츠 파이프라인, 시리즈형 글 구조, 분석 이벤트 추적, 이력서와
검색 경험까지 한 저장소에서 관리한다.

**코어 스택:**
## 핵심 구성

- **프레임워크:** Next.js 16+ (App Router, SSG/SSR)
- **언어:** TypeScript (Strict Mode)
- **스타일링:** Tailwind CSS + CSS Variables (필요한 영역에만 CSS Modules 사용)
- **런타임:** Next.js App Router, React 19, TypeScript strict mode
- **콘텐츠:** `posts/**/index.mdx + meta.json`, 커스텀 webpack MDX 로더
- **UI:** Tailwind CSS, CSS variables, 필요한 구간만 CSS Modules 사용
- **시각화:** Three.js, `@react-three/fiber`, `@react-three/drei`,
Framer Motion
- **품질:** ESLint, Prettier, Vitest, Playwright
- **데이터/분석:** Supabase 기반 조회수 집계, Umami 이벤트 추적

**애니메이션:**
## 개발 시작

- **3D:** Three.js + @react-three/fiber + @react-three/drei
- **모션:** Framer Motion
```bash
npm install
npm run dev
```

**콘텐츠 처리:**
기본 개발 서버는 `internal/scripts/dev-with-agentation.mjs`를 통해 실행된다.
순수 Next.js 개발 서버가 필요하면 `npm run dev:next`를 사용한다.

- **포맷:** MDX + `meta.json` (폴더 기반 콘텐츠 구조)
- **파이프라인:** `@mdx-js/loader` + remark/rehype + syntax highlighting
## 자주 쓰는 명령어

<br />
```bash
npm run dev
npm run dev:next
npm run build
npm run lint
npm run lint:css:syntax
npm run test:unit
npm run test:components
npm run test:e2e
```

## 🏗 시스템 아키텍처
## 콘텐츠 모델

현재 운영 기준 아키텍처는 아래와 같습니다.
블로그 글은 폴더 단위로 관리한다.
중첩 디렉터리를 지원하므로 시리즈 글도 같은 규칙으로 다룬다.

![flow](/public/flow.png)

핵심 포인트:

- 블로그 앱(`eunu.log`)과 분석 대시보드(`Umami`)는 각각 Vercel에 분리 배포합니다.
- 블로그 코드에서는 `NEXT_PUBLIC_UMAMI_URL`, `NEXT_PUBLIC_UMAMI_WEBSITE_ID`만 설정하면 Umami 스크립트가 자동 로드됩니다.
- Umami 커스텀 이벤트는 스크립트 초기화 전 큐에 적재되고, 로드 완료 후 자동으로 flush됩니다.

<br />

## 📂 프로젝트 구조
```text
posts/
├── 어떤-글/
│ ├── index.mdx
│ └── meta.json
└── 시리즈/
└── 에피소드/
├── index.mdx
└── meta.json
```

- 메타데이터 스키마: `src/domains/post/model/frontmatter-schema.ts`
- 콘텐츠 로더: `src/features/blog/services/post-repository.ts`
- MDX 파이프라인: `next.config.mjs`

## 프로젝트 구조

```text
eunu.log/
├── 📁 src/
│ ├── 📁 app/ # 라우트 엔트리 전용 (Next App Router)
│ ├── 📁 core/ # 앱 전역 설정/프로바이더 조합
│ ├── 📁 domains/ # 도메인 계약/타입/스키마
│ ├── 📁 features/ # 기능 모듈(ui/model/services)
│ │ ├── 📁 blog/
│ │ ├── 📁 resume/
│ │ ├── 📁 search/
│ │ └── 📁 home/
│ ├── 📁 shared/ # 공용 모듈(analytics/integrations/layout/seo/testing/ui/types)
│ ├── 📁 components/
│ │ └── 📁 visualization/ # 인터랙티브 알고리즘 시각화 전용
│ └── 📁 styles/ # 전역 스타일과 토큰
├── 📁 tests/
│ └── 📁 e2e/ # Playwright E2E 테스트
├── 📁 internal/
│ ├── 📁 config/ # 내부 lint/spell 설정
│ └── 📁 scripts/ # 내부 자동화/유틸 스크립트
├── 📁 posts/ # 블로그 글(MDX + 메타데이터)
│ └── 📁 [slug]/ # 글 단위 폴더
│ ├── index.mdx # 글 본문
│ └── meta.json # 글 메타데이터
├── 📁 public/ # 정적 에셋
└── 📁 docs/ # 문서
```
src/
├── app/ # App Router 엔트리
├── core/ # 전역 설정과 provider 조합
├── domains/ # 도메인 계약, 타입, 스키마
├── features/ # blog, home, resume, search
├── shared/ # analytics, layout, seo, ui, testing
├── components/visualization/# 시각화 전용 컴포넌트
└── styles/ # 글로벌 스타일과 디자인 토큰

internal/
├── config/ # lint, spell, markdown 설정
└── scripts/ # 개발/콘텐츠 자동화 스크립트

tests/e2e/ # Playwright 시나리오
docs/ # 설계, 운영, 품질 문서
```

## 문서

- 저장소 개요와 아키텍처: `ARCHITECTURE.md`
- 문서 인덱스: `docs/README.md`
- 프런트엔드 기준: `docs/FRONTEND.md`
- 디자인 기준: `docs/DESIGN.md`
- 블로그 품질 기준: `docs/blog-quality-guide.md`

## 작업 원칙

- 콘텐츠 구조는 `posts/**/index.mdx + meta.json` 형태를 유지한다.
- MDX는 `next.config.mjs`의 커스텀 webpack 규칙을 유지한다.
- 시각화가 무거운 UI는 `src/components/visualization/`에 둔다.
- `any`와 임의 Tailwind 값은 추가하지 않는다.
22 changes: 20 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Docs Index

Last updated: 2026-02-28
Last updated: 2026-04-17

This index tracks the active documentation that should stay aligned with the
current codebase. If a document is missing here, add it when the document
becomes part of the maintained workflow.

## Repository-Level Docs

- `AGENTS.md`
- `ARCHITECTURE.md`

## Active Documentation

Expand All @@ -10,19 +19,27 @@ Last updated: 2026-02-28
- `docs/RELIABILITY.md`
- `docs/SECURITY.md`
- `docs/QUALITY_SCORE.md`
- `docs/blog-quality-guide.md`
- Delivery/process:
- `docs/PLANS.md`
- `docs/exec-plans/active/README.md`
- `docs/exec-plans/completed/README.md`
- `docs/exec-plans/tech-debt-tracker.md`
- `docs/guides/agentation-workflow.md`
- `docs/guides/pr-workflow.md`
- `docs/guides/testing-guide.md`
- `docs/guides/ai-collaboration.md`
- `docs/guides/ui-components-guide.md`
- Product/domain:
- `docs/PRODUCT_SENSE.md`
- `docs/product-specs/index.md`
- `docs/analytics/analytics-ga4-schema.md`
- `docs/product-specs/new-user-onboarding.md`
- `docs/design-docs/index.md`
- `docs/design-docs/core-beliefs.md`
- Data/analytics:
- `docs/analytics/analytics-kpi-weekly-template.md`
- `docs/database/db-schema.md`
- `docs/database/supabase-view-count.sql`

## Reference / Research

Expand All @@ -41,3 +58,4 @@ Last updated: 2026-02-28
2. Update `Last updated` when editing policy/process docs.
3. Move stale auto-generated reports to `docs/archive/` instead of deleting context.
4. Keep commands copy-pastable from repository root.
5. Remove or replace stale links when a referenced file no longer exists.
79 changes: 79 additions & 0 deletions docs/blog-quality-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 블로그 품질 가이드

## 브랜드 문장

새로운 비즈니스 확장으로 복잡해지는 내부 시스템을 정리하고, 레거시를 구조적으로 개선하는 플랫폼 엔지니어

## 기본 원칙

- 기술 글은 감상보다 판단 기준이 먼저 보여야 한다.
- 구현 자랑보다 문제 정의와 대안 비교가 먼저 나와야 한다.
- 글 하나가 읽히고 끝나면 안 된다. 같은 문제를 다시 만났을 때 재사용 가능한 기준이 남아야 한다.
- 공개 글은 포트폴리오다. 초안이나 메모 수준의 글은 `private`로 둔다.

## 기술 글 톤

- 기본 문체는 단정형 `하다체`로 쓴다.
- 도입과 마무리만 제한적으로 부드럽게 쓸 수 있다.
- "배웠다", "느꼈다"는 문장만으로 끝내지 않는다. 무엇이 바뀌었는지까지 적는다.
- 추상 표현 대신 수치, 조건, 실패 사례를 우선한다.

## 공개용 엔지니어링 글 템플릿

### 1. 문제와 비즈니스 맥락

- 왜 이 문제가 운영 또는 비즈니스 비용으로 이어졌는가
- 누가 이 결과를 소비하는가

### 2. 시스템 요구사항과 제약

- 성능, 정확성, 복구, 운영 조건
- 리소스 한계나 조직 제약

### 3. 대안 비교와 선택 근거

- 실제로 검토한 대안 2개 이상
- 버린 대안과 이유

### 4. 실제 아키텍처와 구현

- 데이터 흐름
- 핵심 상태 모델
- 중요한 설정값과 선택 이유

### 5. 실패와 수정

- 장애, 병목, 잘못된 가정
- 어떻게 관측했고 어떻게 수정했는가

### 6. 검증과 결과

- 단위 테스트
- 통합 테스트
- 운영 확인 방식
- 수치 결과

### 7. 재사용 가능한 판단 기준

- 다음에도 그대로 가져갈 기준
- 아직 남은 한계

## 공개 정책

- `visibility` 기본값은 `public`이다.
- 시리즈는 기본적으로 공개 자산이 아니라 학습 자산으로 보고, 공개 필요성이 생기기 전까지 `private`로 둔다.
- 엔지니어링 글은 `philosophy`, `design`, `implementation` 평균이 `3.0` 이하이면 `private`로 둔다.
- `featured`는 아래 조건을 모두 만족할 때만 허용한다.
- 엔지니어링 글
- 시리즈 아님
- `brandFit >= 4.0`
- `featured`는 점수만으로 자동 결정하지 않는다. 현재 단계에서는 수동 큐레이션 목록으로 관리한다.

## 리뷰 체크리스트

- 첫 세 문단 안에 문제와 비용이 드러나는가
- 대안 비교가 실제로 존재하는가
- 설정값과 구조가 왜 그렇게 됐는지 설명하는가
- 장애 또는 실패 장면이 포함되어 있는가
- 결과가 수치 또는 운영 변화로 확인되는가
- 읽고 나면 한 문장으로 재사용 가능한 기준이 남는가
30 changes: 23 additions & 7 deletions docs/database/db-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Generated from:
- `docs/database/supabase-view-count.sql`
- `src/shared/integrations/supabase.ts`

Last updated: 2026-02-26
Last updated: 2026-04-16

## Table: `public.views`

Expand All @@ -16,26 +16,42 @@ Last updated: 2026-02-26
| `created_at` | `timestamptz` | no | `now()` | Insert time |
| `updated_at` | `timestamptz` | no | `now()` | Update time |

## Table: `public.view_unique_visitors`

| Column | Type | Null | Default | Notes |
| --- | --- | --- | --- | --- |
| `slug` | `text` | no | - | PK part, post identifier |
| `viewer_fingerprint` | `text` | no | - | PK part, hashed viewer key |
| `last_viewed_at` | `timestamptz` | no | `now()` | Last accepted view time |
| `created_at` | `timestamptz` | no | `now()` | Insert time |
| `updated_at` | `timestamptz` | no | `now()` | Update time |

## RLS and Grants

- RLS enabled on `public.views`.
- RLS enabled on `public.view_unique_visitors`.
- Policy: read access allowed for all (`select` using `true`).
- Grants:
- `select` on `public.views` to `anon`, `authenticated`.
- execute on `increment_view(text)` to `anon`, `authenticated`.
- execute on `increment_view(text, text, integer)` to `anon`, `authenticated`.

## Function: `public.increment_view(slug_input text)`
## Function: `public.increment_view(slug_input text, viewer_fingerprint_input text default null, dedupe_window_seconds_input integer default 86400)`

- Language: `plpgsql`
- Security: `security definer`
- Behavior:
1. Insert slug with `count=1`.
2. On conflict, increment count and update `updated_at`.
3. Return latest count (`bigint`).
1. Normalize slug and fingerprint input.
2. If fingerprint exists, enforce dedupe window by `view_unique_visitors.last_viewed_at`.
3. Increment `views.count` only when outside dedupe window.
4. Return latest count (`bigint`).

## Type Mapping (App)

`src/shared/integrations/supabase.ts` defines:

- Table row type for `views`.
- RPC signature: `increment_view.Args.slug_input: string`, `Returns: number`.
- RPC signature:
- `increment_view.Args.slug_input: string`
- `increment_view.Args.viewer_fingerprint_input?: string`
- `increment_view.Args.dedupe_window_seconds_input?: number`
- `Returns: number`
Loading
Loading