Skip to content

snud2022/snu-design-week-2025

Repository files navigation

SNU DESIGN WEEK 2025 - WRAP UP

logo


OG Image

🔗 웹사이트 바로가기 🔗

📋 목차

📦 프로젝트 소개

SNU DESIGN WEEK 2025 - WRAP UP

WRAP UP은 4년간 모아온 짐을 꾸리는 이사의 현장을 테마로 한 서울대학교 디자인과 졸업 전시 웹사이트입니다.
물리 엔진 기반 그래픽을 통해 독특한 사용자 경험을 제공합니다.

📄 페이지 소개

총 6개의 주요 페이지로 구성되어 있으며, 모든 페이지는 반응형 디자인을 지원합니다.

Main Page

2025-12-20.6.08.35.1.mov

Matter.js 기반 인터랙티브 물리 시뮬레이션을 제공합니다. 전시 명을 딴 WRAP UP 알파벳 형태의 물리 객체들이 중력에 따라 움직입니다.

About Page

2025-12-20.6.09.27.mov

전시 소개 및 조직 정보를 제공하는 페이지입니다.

Works Page

2025-12-20.6.11.22.1.mov

Notion Database에서 가져온 졸업 작품 목록을 갤러리로 표시합니다. 상단 필터로 class별 작품을 필터링할 수 있으며, 작품 클릭 시 상세 페이지로 이동합니다.

People Page

2025-12-20.6.11.49.mov

학생 정보를 물리 엔진 기반 그리드로 표시합니다. 각 셀마다 독립적인 물리 엔진 인스턴스를 생성하며, 셀 내 class 파츠를 선택하면 상세 페이지로 이동합니다.

Works Detail Page

works.detail.1.mov

작품 클릭 시 이동하는 상세 페이지입니다. Notion 페이지를 직접 렌더링하여 작품의 상세 정보를 제공합니다.

Program Page

2025-12-20.6.16.04.mov

전시 기간 동안 진행되는 프로그램 일정을 안내합니다.

Partners Page

2025-12-20.6.16.33.mov

전시를 함께하는 파트너사와 동문 후원 현황을 소개합니다. 파트너 로고 및 후원 정보를 카드 형태로 표시합니다.

반응형 디자인

모든 페이지는 다양한 화면 크기에 최적화되어 있습니다:

  • 모바일 (Mobile): 0px ~ 599px
  • 태블릿 (Tablet): 600px ~ 1279px
  • 데스크톱 (Desktop): 1280px 이상

@snud2025/ui 패키지의 breakpoints 상수를 사용하여 일관된 반응형 디자인을 구현했습니다.

📁 프로젝트 구조

이 프로젝트는 Turborepo 모노레포 구조로 구성되어 있습니다.

snu-design-week-2025/
├── apps/
│   ├── web/                # 온라인 전시 웹사이트 (Next.js)
│   │   ├── app/            # Next.js App Router
│   │   │   ├── about/      # About 페이지
│   │   │   ├── people/     # People 페이지
│   │   │   ├── works/      # Works 페이지 및 상세 페이지
│   │   │   ├── program/    # Program 페이지
│   │   │   ├── partners/   # Partners 페이지
│   │   │   └── page.tsx    # 메인 페이지
│   │   ├── components/     # 공통 컴포넌트
│   │   ├── services/       # API 호출 로직
│   │   ├── utils/          # 유틸리티 함수
│   │   ├── constants/      # 상수 정의
│   │   ├── types/          # 타입 정의
│   │   └── public/         # 정적 파일
│   └── docs/               # 문서 사이트 (Next.js)
├── packages/
│   ├── api/                # Notion API 호출 로직 및 에러 처리
│   ├── ui/                 # 공유 React 컴포넌트 라이브러리
│   ├── eslint-config/      # ESLint 설정
│   └── typescript-config/  # TypeScript 설정
├── pnpm-workspace.yaml     # pnpm 워크스페이스 설정
├── turbo.json              # Turborepo 설정
└── vercel.json             # Vercel 설정

의존성 방향: packages → apps 순서로 단방향 의존성을 유지합니다.

pnpm Workspace 및 Catalog

pnpm workspace를 사용하여 모노레포를 관리합니다. catalog 기능으로 의존성 버전을 중앙에서 관리합니다.

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
catalog:
  next: "15.5.7"
  react: "^19.1.0"
{
  "dependencies": {
    "next": "catalog:",
    "@snud2025/ui": "workspace:*"
  }
}

⚡ 주요 기능

1. Notion Database 연동 및 Notion 페이지 렌더링

Notion Database 연동

작품과 학생 정보는 Notion Database에서 관리되며, 웹사이트에서 실시간으로 가져옵니다.

  • Works DB (NOTION_SNU_WORKS_DB): 작품 정보
  • People DB (NOTION_SNU_PEOPLE_DB): 학생 정보

Next.js App Router의 서버 컴포넌트에서 데이터를 페칭합니다.

Notion 페이지 렌더링

react-notion-x를 사용하여 Notion 페이지를 React 컴포넌트로 렌더링합니다.

Notion 이미지 처리

Notion API에서 받아온 이미지 URL은 만료 시간(약 1시간)이 있어 직접 사용 시 이미지가 깨질 수 있습니다. 이를 방지하기 위해 Notion 웹 게시 링크로 변환하여 사용합니다.

// packages/ui/src/utils/getImageUrl.ts
export function getImageUrl(imageUrl: string, pageId: string): string {
  return `https://${NOTION_SITE_DOMAIN}/image/${encodeURIComponent(imageUrl)}?table=block&id=${pageId}&cache=v2`;
}

Notion 데이터 변환

Notion API의 동적 타입 데이터를 안전하게 변환하는 유틸리티 함수를 사용합니다:

// apps/web/app/works/utils/transformWorks.ts
export function transformWork(notionWork: NotionWork): ProjectDetail {
  try {
    const properties = notionWork.properties;

    // 기본 정보 추출
    const nameKo = extractText(properties.작품이름) || "제목 없음";
    const nameEn = extractText(properties.작품이름_영문) || "Untitled";
    const studentNameKo = extractText(properties.학생이름) || "";
    const studentNameEn = extractText(properties.학생이름_영문) || "";
    const email = properties.Email?.email || "";
    const instagram = extractText(properties["인스타 아이디"]) || "";

    // filterIndex: 수업 정보에서 추출
    let filterIndex = 0;
    if (
      typeof properties.filterIndex === "object" &&
      "number" in properties.filterIndex &&
      properties.filterIndex.number !== null
    ) {
      filterIndex = properties.filterIndex.number;
    } else if (properties.수업) {
      filterIndex = getFilterIndexFromClass(properties.수업);
    }

    const projectType = getCategoryByIndex(filterIndex) || "BRAND";
    const thumbnailUrl = extractCoverUrl(notionWork);

    // 통합 프로젝트 여부 판단
    const isIntegratedProject =
      properties.다른작품?.relation && properties.다른작품.relation.length > 0
        ? false
        : undefined;

    return {
      id: notionWork.id,
      projectType,
      filterIndex,
      nameKo,
      nameEn,
      studentNameKo,
      studentNameEn,
      thumbnailUrl,
      email,
      instagram,
      isIntegratedProject,
    };
  } catch {
    // 기본값 반환으로 안정성 보장
    return {
      /* 기본값 */
    };
  }
}

2. 캐싱 전략

Next.js의 unstable_cache를 활용하여 데이터를 캐싱하고, 개발 환경에서는 캐시를 비활성화하여 실시간 업데이트를 확인할 수 있습니다:

// apps/web/services/common/getDatabase.ts
export const getNotionDatabase = cache(
  async <T>(
    client: Client,
    databaseId: string,
    cacheKey?: string,
    revalidateSeconds
  ): Promise<T[] | []> => {
    return unstable_cache(
      async () => {
        return await queryNotionDatabase<T>(client, databaseId, sorts);
      },
      [
        cacheKey || `notion-database-${databaseId}`,
        ...(process.env.NODE_ENV === "development"
          ? [Date.now().toString()]
          : []),
      ],
      {
        revalidate:
          process.env.NODE_ENV === "development" ? false : revalidateSeconds,
        tags: ["notion-database", `notion-database-${databaseId}`],
      }
    )();
  }
);

프로덕션에서는 10분(600초) 캐시를 유지하고, 개발 환경에서는 캐시를 비활성화합니다.

3. Matter.js 물리 엔진

Matter.js 물리 엔진은 두 곳에서 사용됩니다:

1. 메인 페이지 (PhysicsScene)

홈페이지의 인터랙티브 물리 시뮬레이션입니다.
전시명 "WRAP UP" 알파벳 형태의 물리 객체들이 중력에 따라 움직이며, 사용자의 마우스 상호작용을 지원합니다.

구성 요소:

  • Matter.js Engine, Render, Runner를 사용한 물리 시뮬레이션
  • Canvas 기반 렌더링
  • 반응형 브레이크포인트에 따른 동적 스케일 조정
  • 마우스 상호작용을 위한 MouseConstraint
  • 벽(Walls)을 통한 경계 처리

2. People 페이지 (PhysicsCell)

People 페이지의 각 학생 셀마다 독립적인 물리 엔진 인스턴스를 생성합니다.
학생이 수강한 class를 상징하는 파츠들이 물리 객체로 표시되며, DOM 요소와 동기화됩니다.

구성 요소:

  • 각 셀마다 독립적인 Matter.js Engine 인스턴스
  • DOM 요소를 물리 객체와 동기화 (Canvas 렌더러 미사용)
  • 벽을 통한 셀 경계 처리

4. API 에러 핸들러

Notion API 호출 시 발생하는 에러를 타입별로 구분하여 처리합니다.

1단계: 에러 타입 정의

// packages/api/src/error.ts
export class ClientError extends Error {
  constructor(
    public status: number,
    message: string
  ) {
    super(message);
    this.name = "ClientError";
  }
}

export class ServerError extends Error {
  constructor(
    public status: number,
    message: string
  ) {
    super(message);
    this.name = "ServerError";
  }
}

export class NetworkError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NetworkError";
  }
}

2단계: 에러 파싱 함수 구현

// packages/api/src/error.ts
export function parseError(err: unknown): never {
  if (isNotionClientError(err)) {
    if (err instanceof APIResponseError) {
      const status = err.status;
      // 클라이언트 에러 (4xx)
      if (status >= 400 && status < 500) {
        throw new ClientError(status, message);
      }
      // 서버 에러 (5xx)
      if (status >= 500) {
        throw new ServerError(status, message);
      }
    }
    // 네트워크 에러
    throw new NetworkError(err.message);
  }
  // 알 수 없는 에러
  throw new Error("Unknown error occurred");
}

3단계: API 호출 시 에러 처리 적용

// packages/api/src/request.ts
export async function queryNotionDatabase<T>(
  client: Client,
  databaseId: string
): Promise<T[]> {
  try {
    const response = await client.databases.query({
      database_id: databaseId,
    });
    return response.results as T[];
  } catch (error) {
    parseError(error); // 에러 타입에 따라 적절히 처리
  }
}

이를 통해 클라이언트 에러, 서버 에러, 네트워크 에러를 명확히 구분하여 각각에 맞는 에러 핸들링이 가능합니다.

⚙️ 개발 환경 설정

환경 변수

프로젝트 루트에 .env.local 파일을 생성하고 다음 변수를 설정합니다:

# Notion API
NOTION_DB_API_KEY=your_notion_api_key
NOTION_SNU_WORKS_DB=your_works_database_id
NOTION_SNU_PEOPLE_DB=your_people_database_id

NEXT_PUBLIC_NOTION_SITE_DOMAIN=your_notion_site_domain

설치 및 실행

# 의존성 설치
pnpm install

# 개발 서버 실행 (모든 앱)
pnpm dev

# 특정 앱만 실행
pnpm dev --filter=web
pnpm dev --filter=docs

# 빌드
pnpm build

# 린트
pnpm lint

# 포맷팅
pnpm format

이 프로젝트는 서울대학교 디자인과 졸업 전시를 위한 프로젝트입니다.

About

2025 서울대학교 디자인과 졸업주간 전시 사이트

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •