Fullstack 53

heo4·2026년 6월 30일

Fullstack

목록 보기
8/70

풀스택

포트폴리오 작업

모바일 초대장 추가사항 및 보완

신규 작업 안내사항 섹션 추가

포토부스, 전세버스, 답례품이나 안내할 정보가 있으면 추가가 가능

React + TypeScript + Cloudflare Pages로 만드는 모바일 청첩장 SaaS 개발 기록입니다.


요약

오늘은 크게 두 가지 방향으로 작업했습니다.

  1. 커버 텍스트 자유 배치 기능 (Canva 스타일 편집기)
  2. 안내사항 섹션 신규 구현 + 각종 버그 수정 + 폰트 5종 추가

1. 커버 텍스트 자유 배치 (Canva 스타일)

청첩장 커버 위에 텍스트(이름, 날짜, 장소 등)를 원하는 위치로 자유롭게 드래그해서 배치할 수 있는 기능을 구현했습니다.

핵심 구조

Cover 섹션
├── CoverComponent (템플릿 — 기존 텍스트 그대로 유지)
├── OverlayLayer (fog/vignette 효과)
└── CoverTextOverlay (390×690 고정 박스 안에 배치)
    ├── 격자 레이어 (editMode && showGrid)
    └── TextBlock × N (드래그 이동 / 크기 / 회전)

CoverTextBlock 타입

export interface CoverTextBlock {
  id: string;
  content?: string;       // 미설정 시 결혼 정보 자동 표시
  x: number;             // 중심 X (0~100%)
  y: number;             // 중심 Y (0~100%)
  fontSize: number;      // px
  color: string;
  align: 'left' | 'center' | 'right';
  visible: boolean;
  rotation?: number;     // deg
  letterSpacing?: string;
}

드래그 3가지 모드

포인터 이벤트 하나로 이동 / 크기 / 회전을 처리했습니다.

type DragMode = 'move' | 'resize' | 'rotate';

// 이동: delta를 컨테이너 크기로 나눠서 % 변환
const dx = ((e.clientX - d.startX) / d.containerW) * 100;
const dy = ((e.clientY - d.startY) / d.containerH) * 100;

// 크기: 오른쪽+아래 드래그 → 폰트 크기 증가
const delta = (e.clientX - d.startX + e.clientY - d.startY) * 0.25;

// 회전: 블록 중심 기준 atan2 계산
const angleRad = Math.atan2(e.clientY - centerY, e.clientX - centerX);
const angleDeg = angleRad * (180 / Math.PI) + 90;

핸들 배치 (Canva 스타일)

  • 우상단 코너: 회전 핸들 (↻)
  • 우하단 코너: 크기 조절 핸들 (⤡)
  • 선택 툴팁: ✏️ 편집 / ⟳ 초기화 / ✕ 숨기기

격자 + 스냅

const SNAP_X = [0, 25, 33.333, 50, 66.667, 75, 100];
const SNAP_Y = [0, 16.667, 25, 33.333, 50, 66.667, 75, 83.333, 100];
const SNAP_THRESHOLD = 2.5; // 2.5% 이내 자동 흡착

function snapValue(value: number, points: number[]): number {
  for (const p of points) {
    if (Math.abs(value - p) < SNAP_THRESHOLD) return p;
  }
  return value;
}

격자는 수직 3분할(33%, 50%, 66%)과 수평 7분할로 구성하고, 중앙선을 더 밝게 강조했습니다.

편집 모달 위치 문제 해결

편집 모달을 fixed로 구현했는데 조상 요소에 transform이 있으면 fixed 포지셔닝이 그 요소 기준으로 잡히는 문제가 있었습니다.

문제: DesignTab 안에 fixed 모달 → 조상 transform에 갇힘
해결: 모달 state/JSX를 MakePage 루트 레벨로 끌어올림
      (전체화면 미리보기 모달과 동일한 패턴)

편집 미리보기 ↔ 실제 렌더 위치 일치

편집 모달(300×532)과 실제 Cover 섹션(min-h-screen = 100vh)의 높이 비율이 달라서 저장된 % 위치가 달라 보이는 문제가 있었습니다.

해결: Cover.tsx에서 CoverTextOverlay를 390×690 고정 박스 안에 배치
      편집 모달도 300×532 (= 390×690의 77% 축소, 동일 비율)
      → 어디서 렌더해도 동일한 % 기준점 사용
{textOverlayActive && (
  <div style={{
    position: 'absolute', top: 0, left: '50%',
    transform: 'translateX(-50%)',
    width: 390, height: 690,
    pointerEvents: 'none',
  }}>
    <CoverTextOverlay blocks={design.coverTextBlocks} editMode={false} />
  </div>
)}

오버레이 역할 변경

처음에는 오버레이 활성 시 기존 커버 텍스트를 CSS로 숨기고 오버레이 텍스트로 대체하는 방식이었습니다. 그런데 기존 디자인이 사라지는 게 어색했습니다.

변경 전: coverTextOverlay ON → 기존 .cover-text-layer 숨김 + 오버레이로 대체
변경 후: 기존 텍스트 그대로 유지 + 오버레이는 추가 텍스트 레이어로만 동작
         DEFAULT_COVER_TEXT_BLOCKS 모두 visible: false (처음엔 비어있음)

2. 안내사항(Notice) 섹션 신규 구현

하객에게 전달할 주차 안내, 드레스 코드, 셔틀 정보 등을 편집할 수 있는 섹션을 새로 만들었습니다.

표시 형식 3종

  • 탭: 카테고리를 탭으로 전환
  • 아코디언: 클릭해서 펼치기
  • 슬라이드: 좌우 스와이프

샘플 문구 모달

카테고리별(주차/식사/대중교통/드레스코드 등) 샘플 문구를 모달로 제공해서 클릭하면 바로 입력되도록 했습니다.


3. 오시는 길 — 지도앱 버튼 개편

기존 카카오맵·네이버·구글 3버튼에서 용도별 2행으로 개편했습니다.

길찾기 행:  [네이버지도] [카카오맵] [구글지도]   ← 대중교통/도보/국제 사용자
내비게이션: [티맵]       [카카오내비]             ← 자차 하객 딥링크

티맵은 tmap:// 딥링크로 앱을 직접 실행합니다.


4. 인기 한국어 폰트 5종 추가

프리텐다드가 없다는 걸 뒤늦게 발견했습니다. 국내에서 가장 많이 쓰이는 폰트들을 추가했습니다.

폰트용도CDN
Pretendard제목+본문jsDelivr (Light~Bold 5단계)
롯데마트 드림제목noonnu CDN
에스코어 드림 (SCDream)제목+본문noonnu CDN (5단계)
야놀자야체제목noonnu CDN
넥슨 Lv.1 고딕본문noonnu CDN

@font-face는 index.html의 <style> 블록에 직접 선언했습니다. Google Fonts에 없는 폰트들이라 CDN 직접 참조 방식을 사용했습니다.

<!-- 프리텐다드 -->
@font-face {
  font-family: 'Pretendard';
  src: url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/woff2-subset/Pretendard-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

5. 기타 버그 수정

전체화면 미리보기 인트로 버그

전체화면 미리보기에서 인트로 효과가 샘플 데이터 기준으로 재생되는 버그가 있었습니다.

원인: IntroRenderer가 WeddingContext.Provider 바깥에 렌더되어
      formData 대신 상위 샘플 컨텍스트를 읽음
해결: IntroRenderer를 WeddingContext.Provider 안으로 이동

편집 모달 핸들 잘림

편집 모달 컨테이너에 overflow: hidden이 있어서 선택 툴팁(-58px 위)과 회전 핸들(-36px 위)이 잘리는 문제가 있었습니다.

해결: 외부 wrapper(overflow: visible) + 내부 이미지 클립 div(overflow: hidden) 분리
      → 커버 이미지는 borderRadius로 clip, 핸들은 자유롭게 밖으로 나옴

느낀 점

  • position: fixed + 조상 transform 조합은 항상 조심해야 합니다. 모달류 컴포넌트는 가능하면 루트 레벨에 렌더하는 게 안전합니다.
  • 편집 좌표계와 실제 렌더 좌표계를 일치시키는 것이 생각보다 까다로웠습니다. 결국 "공통 기준 박스(390×690)" 하나를 정해서 양쪽이 동일한 기준을 사용하도록 강제하는 방법으로 해결했습니다.
  • Canva나 Figma 같은 편집기의 핸들 UX가 왜 코너에 있는지 이번에 직접 구현해보면서 이해했습니다. 상단 중앙 회전 핸들은 텍스트가 상단에 있을 때 아무것도 안 보입니다.

기술 스택

  • Frontend: React 18 + TypeScript + Tailwind CSS
  • Backend: Cloudflare Workers + D1 + R2 + KV
  • 배포: Cloudflare Pages
  • 이미지: react-easy-crop + Canvas API

0개의 댓글