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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

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

- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Lint
run: pnpm lint

- name: Unit tests
run: pnpm exec vitest run

- name: Build
run: pnpm build

- name: E2E smoke
run: pnpm exec playwright test tests/main.spec.ts tests/contact.mobile.spec.ts --project="Desktop Chrome" --project="Mobile Chrome"
44 changes: 44 additions & 0 deletions .github/workflows/performance-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Performance Regression

on:
workflow_dispatch:
schedule:
- cron: "0 23 * * 1"

jobs:
benchmark:
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

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

- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Run quick performance benchmark
run: pnpm test:perf:quick

- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-benchmark
path: |
test-results/
test-results/perf-report/
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ certificates
test-results
playwright-report
playwright-video

# local agent tooling
.agents/
.claude/
.omx/
skills-lock.json
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
## ✨ 주요 기능

- 🏠 **Home**: 간략한 자기소개 및 보유 기술 스택
- 📁 **Projects**: 진행한 프로젝트 포트폴리오 (5개 프로젝트)
- 📁 **Projects**: 케이스 스터디 중심 프로젝트 포트폴리오
- 프로젝트 상세 페이지 (URL 및 모달 뷰 지원)
- 프로젝트별 기술 스택, 챌린지, 해결책, 성과 등 상세 정보
- 이미지 갤러리 (라이트박스 기능 지원)
Expand All @@ -27,7 +27,7 @@
- ✅ **M1**: 디자인 토큰 재정의, 홈/레이아웃 비주얼 시스템 개편
- ✅ **M2**: About/Tech Stack 페이지를 데이터 중심 내러티브 구조로 확장
- ✅ **M3**: Projects 목록/상세를 스토리 중심 정보 구조로 개선
- **M4**: 접근성/SEO/성능 하드닝 + 린트 워크플로우 정비
- 🟡 **M4**: 접근성/메타데이터/성능 하드닝과 자동화 검증 정비 진행 중

## 🌐 배포

Expand Down Expand Up @@ -61,6 +61,7 @@
- **Pattern**: Feature-Sliced Design (FSD)
- **Structure**: pages-layer, features, shared
- **Type Safety**: @t3-oss/env-nextjs
- **Architecture Note**: [`docs/architecture-tradeoffs.md`](docs/architecture-tradeoffs.md)

## 📁 프로젝트 구조

Expand Down Expand Up @@ -131,9 +132,11 @@ pnpm install

```env
# Email Configuration
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
EMAIL_TO=recipient@example.com
NEXT_MAIL_ADDRESS=your-email@gmail.com
NEXT_APP_PASSWORD=your-app-password

# Analytics
NEXT_PUBLIC_GOOGLE_ANALYTICS=G-XXXXXXXXXX
```

### 개발 서버 실행
Expand Down Expand Up @@ -180,14 +183,14 @@ pnpm test:e2e-log
pnpm lint && pnpm test -- --run && pnpm build
```

## 📌 포트폴리오 프로젝트
## 📌 대표 프로젝트

### 1. POCAZ
### 1. POCAZ Remake

- **설명**: 아이돌 포토카드 전문 거래 플랫폼
- **역할**: 국내외 8000억 규모 아이돌 굿즈 시장을 겨냥한 포토카드 리셀 거래 플랫폼 개발
- **기술**: React, JavaScript, Tailwind CSS, Express.js, MySQL
- **링크**: [GitHub](https://github.com/TEAM-POCAZ/PocaZ)
- **설명**: 아이돌 포토카드 리셀 거래를 전문 UX로 다시 설계한 리메이크 프로젝트
- **역할**: 1인 풀스택 개발 (Next.js, 상태관리, API 연동)
- **기술**: React, Next.js, StyleX, Elysia.js, PostgreSQL, Prisma, Supabase, Bun.js
- **링크**: [GitHub](https://github.com/Ring-wdr/pocaz-remake) · [Demo](https://pocaz-remake.vercel.app/)

### 2. 법률사무소 대도

Expand All @@ -206,10 +209,24 @@ pnpm lint && pnpm test -- --run && pnpm build
### 4. 역대카

- **설명**: 렌트카 가격 비교 서비스
- **역할**: 렌트사 비교 및 최저가 추천 서비스 프로토타입 개발
- **기술**: Next.js, Supabase, Tailwind CSS, TypeScript
- **역할**: 개인 프로젝트 풀스택 개발 (Next.js, Supabase)
- **기술**: Next.js, Supabase, Prisma, Tailwind CSS, TypeScript
- **링크**: [웹사이트](https://alltime-car.com/)

### 5. 프론트엔드 주니어 스터디

- **설명**: 15주 학습 커리큘럼과 실습 기록을 구조화한 공개 학습 저장소
- **역할**: 커리큘럼 설계 및 학습 자료 정리
- **기술**: TypeScript, Bun.js, CSS
- **링크**: [GitHub](https://github.com/Ring-wdr/frontend-junior-study) · [Demo](https://ring-wdr.github.io/frontend-junior-study/)

### 6. react-devtool-cli

- **설명**: Playwright 기반 브라우저 세션 위에서 React inspection과 profiler 분석을 자동화하는 agent-first CLI
- **역할**: CLI 설계 및 구현, Playwright 전송 계층 구성, snapshot-aware inspection 워크플로우 설계
- **기술**: React, Playwright, Command Line, JavaScript
- **링크**: [GitHub](https://github.com/Ring-wdr/react-devtool-cli) · [npm](https://www.npmjs.com/package/react-devtool-cli)

## 💡 주요 특징

### Feature-Sliced Design (FSD)
Expand Down
45 changes: 45 additions & 0 deletions docs/architecture-tradeoffs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Architecture & Trade-offs

## What this portfolio optimizes for
- **Recruiter readability first**: the repo favors clear case-study delivery over generic portfolio animations or novelty.
- **Small-system maintainability**: feature/page/shared boundaries keep content, UI, and route concerns separated without introducing unnecessary framework layers.
- **Evidence-based frontend craft**: metadata, tests, responsive behavior, and performance checks are treated as part of the product, not polish.

## Chosen stack
- **Next.js App Router** for route-level metadata, localized pages, static generation, and built-in SEO surfaces.
- **React 19** for server/client composition and view-transition-driven interactions.
- **TypeScript + Zod** for keeping recruiter-visible content and operational inputs explicit.
- **Tailwind CSS** for fast iteration on a consistent visual system.
- **next-intl** for bilingual delivery without duplicating whole page implementations.
- **Vitest + Playwright** for fast content/data regression checks plus recruiter-critical route smoke coverage.

## Why this shape
### 1. Feature/page/shared boundaries instead of a larger app framework
This codebase is content-heavy and interaction-light compared with a SaaS product. The current structure keeps route composition simple while still separating page-level composition, feature logic, and shared UI/constants.

### 2. Project data in shared constants
Project stories drive multiple surfaces: homepage, project list, detail pages, README, metadata, and tests. Centralizing those facts reduces trust-breaking drift in roles, dates, and outcomes.

### 3. Route-native metadata instead of afterthought SEO
Metadata is defined at the route level so each page can emit its own canonical URL, locale alternates, and share copy. This makes the portfolio easier to search, preview, and review in hiring workflows.

## Trade-offs accepted
- **Static constants over CMS**: simpler and safer for a personal portfolio, but updates require code changes.
- **In-memory contact throttling**: low-cost spam protection suitable for a portfolio, but not durable across distributed serverless instances.
- **Playwright smoke over full browser matrix in CI**: keeps CI fast while still covering the recruiter-critical paths. Wider browser confidence is preserved for local/manual runs.
- **Generated OG images over custom design assets**: faster to maintain and always aligned with current titles, but visually less bespoke than hand-crafted social cards.

## Alternatives considered
- **Headless CMS**: rejected for now because it increases operational overhead without improving hiring signal enough for this repo.
- **More abstract data model for projects**: rejected because the current domain is small; a thin canonical source is enough.
- **External anti-spam service**: rejected because it adds keys, vendor setup, and maintenance for a low-volume form.

## Consequences
- Updating portfolio stories is straightforward, but still requires discipline around canonical project facts.
- The repo now has stronger trust signals for interviews: documented trade-offs, smoke-tested routes, and metadata/CI coverage.
- Future scaling pressure will most likely appear in the contact flow and project content editing path, not in the rendering layer.

## Follow-ups
- Add durable rate limiting or bot protection if unsolicited submissions grow.
- Expand project-level metrics only when numbers are interview-defensible.
- Consider localized project data if English copy needs to diverge meaningfully from Korean.
16 changes: 15 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";

const eslintConfig = [...nextCoreWebVitals, ...nextTypescript];
const eslintConfig = [
{
ignores: [
".agents/**",
".claude/**",
".next/**",
".omx/**",
"node_modules/**",
"test-results/**",
"tests-log/**",
],
},
...nextCoreWebVitals,
...nextTypescript,
];

export default eslintConfig;
22 changes: 19 additions & 3 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"projects": "Projects",
"techStack": "Tech Stack",
"about": "About Me",
"contact": "Contact"
"contact": "Contact",
"menu": "Open menu",
"closeMenu": "Close menu"
},
"Footer": {
"allRightsReserved": "All rights reserved.",
Expand Down Expand Up @@ -178,7 +180,21 @@
"send": "Send",
"sending": "Sending...",
"success": "Message sent successfully!",
"error": "Failed to send message. Please try again."
"error": "Failed to send message. Please try again.",
"namePlaceholder": "Your name",
"emailPlaceholder": "you@company.com",
"company": "Company / Team",
"companyPlaceholder": "Company or team",
"purpose": "Purpose",
"purposeOptions": {
"jobOpportunity": "Job opportunity / interview",
"projectInquiry": "Project inquiry",
"collaboration": "Collaboration",
"other": "Other"
},
"messagePlaceholder": "Tell me about the role, project, or collaboration you're considering.",
"website": "Website",
"privacyNote": "I will only use this information to reply to your inquiry."
},
"ProjectDetailPage": {
"period": "Period",
Expand All @@ -198,7 +214,7 @@
"achievements": "Achievements",
"metrics": "Key Metrics",
"feedback": "User Feedback",
"improvements": "Improvements",
"improvements": "Learnings / Next Steps",
"screenshots": "Screenshots",
"backToProjects": "Back to Projects"
}
Expand Down
22 changes: 19 additions & 3 deletions messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"projects": "프로젝트",
"techStack": "기술 스택",
"about": "소개",
"contact": "연락하기"
"contact": "연락하기",
"menu": "메뉴 열기",
"closeMenu": "메뉴 닫기"
},
"Footer": {
"allRightsReserved": "All rights reserved.",
Expand Down Expand Up @@ -178,7 +180,21 @@
"send": "보내기",
"sending": "전송 중...",
"success": "메시지가 성공적으로 전송되었습니다!",
"error": "메시지 전송에 실패했습니다. 다시 시도해주세요."
"error": "메시지 전송에 실패했습니다. 다시 시도해주세요.",
"namePlaceholder": "성함을 알려주세요",
"emailPlaceholder": "reply@example.com",
"company": "회사 / 팀",
"companyPlaceholder": "소속이 있다면 알려주세요",
"purpose": "문의 목적",
"purposeOptions": {
"jobOpportunity": "채용 / 인터뷰 제안",
"projectInquiry": "프로젝트 문의",
"collaboration": "협업 제안",
"other": "기타"
},
"messagePlaceholder": "어떤 포지션인지, 어떤 프로젝트인지, 어떤 도움이 필요한지 알려주세요.",
"website": "웹사이트",
"privacyNote": "남겨주신 정보는 답변과 후속 연락을 위해서만 사용합니다."
},
"ProjectDetailPage": {
"period": "기간",
Expand All @@ -198,7 +214,7 @@
"achievements": "성과",
"metrics": "주요 지표",
"feedback": "사용자 피드백",
"improvements": "개선 사항",
"improvements": "배운 점 / 다음 단계",
"screenshots": "스크린샷",
"backToProjects": "프로젝트 목록으로 돌아가기"
}
Expand Down
9 changes: 9 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path";

const PORT = process.env.PORT || 3000;
const baseURL = `http://localhost:${PORT}`;
const mobileTag = /@mobile/;

export default defineConfig({
timeout: 30 * 1000,
Expand All @@ -25,15 +26,23 @@ export default defineConfig({
projects: [
{
name: "Desktop Chrome",
grepInvert: mobileTag,
use: { ...devices["Desktop Chrome"] },
},
{
name: "Desktop Firefox",
grepInvert: mobileTag,
use: { ...devices["Desktop Firefox"] },
},
{
name: "Desktop Safari",
grepInvert: mobileTag,
use: { ...devices["Desktop Safari"] },
},
{
name: "Mobile Chrome",
grep: mobileTag,
use: { ...devices["Pixel 7"] },
},
],
});
21 changes: 20 additions & 1 deletion src/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
export { AboutPage as default } from "@/pages-layer/about";
import type { Metadata } from "next";
import { AboutPage } from "@/pages-layer/about";
import { buildPageMetadata, type AppLocale } from "@/shared/constant/site";

export async function generateMetadata({
params,
}: PageProps<"/[locale]/about">): Promise<Metadata> {
const { locale } = await params;

return buildPageMetadata({
locale: locale as AppLocale,
pathname: "/about",
title: "About | Manjoong Kim",
description:
"Career timeline, working principles, and frontend strengths behind Manjoong Kim's portfolio work.",
keywords: ["about frontend engineer", "career timeline", "working principles"],
});
}

export default AboutPage;
Loading
Loading