FE 도메인 테크스펙 ‐ 공통 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

1. 배경 (Background)

프로젝트 목표 (Objective)

  • 여러 도메인에서 반복되는 구현(요청/캐싱/에러처리/폼/모달)을 공통 규칙으로 통일하여 UX 일관성, 버그 감소, 개발 생산성을 확보함
    • 핵심 결과 (Key Result) 1: 도메인마다 다른 방식으로 구현되기 쉬운 로딩/에러/토큰 처리를 한 가지 규칙으로 통일함 (Axios 인스턴스/인터셉터 표준)
    • 핵심 결과 (Key Result) 2: OCR/AI 업로드 같은 비동기 흐름에서 대기 모달/실패 토스트/완료 모달 UX를 페이지 상관없이 동일하게 제어함 (Zustand 전역 UI 상태)
    • 핵심 결과 (Key Result) 3: 버튼/입력/모달/토스트 등 공통 UI를 재사용해 디자인 일관성 + 개발 속도 확보 (shadcn/ui)
    • 핵심 결과 (Key Result) 4: 첨부 파일 업로드 절차가 모든 도메인에서 동일한 파이프라인을 사용함 (Presigned URL 발급 → S3 직접 업로드 → 파일 메타 저장)

문제 정의 (Problem)

  • Devths는 캘린더/게시판/채팅/챗봇 등 기능이 많아 인증/에러/로딩/첨부가 여러 화면에 반복됨
    • 이것을 화면마다 다르게 구현하면 UX 편차와 버그가 누적됨

가설 (Hypothesis)

  • Common 도메인에서 전역 UI(Zustand), 서버 캐시(TanStack Query), 네트워크(Axios), 파일 업로드 훅을 표준화하면 도메인 개발자는 비즈니스 로직에만 집중할 수 있고, UX가 안정됨

관련 자료

  • 공통 API 명세(프리사인드/메타 저장/에러 응답)
  • Zustand/axios 표준화 근거(전역 UI 상태, 인터셉터 기반 공통 처리)

2. 목표가 아닌 것 (Non-goals) (Optional)

  • 도메인 고유 정책을 Common으로 흡수하지 않음
    • 게시판 검색정책, 채팅 읽음처리 정책, 캘린더 세부 모델 규칙 등
  • 서버가 파일을 직접 받아 중계하는 업로드 방식은 다루지 않음
    • Devths는 FE→S3 직접 업로드 분리를 전제로 함
  • 디자인 시스템을 새로 구축하지 않음
    • shadcn/ui 기반으로 필요한 공통 UI만 구축

3. 설계 및 기술 자료 (Architecture and Technical Documentation)

아키텍처 개요 (Architecture Overview)

  • 프레임워크/라이브러리
    • React + Next.js
      • App Router 기반으로 라우팅/레이아웃을 표준화할 수 있어 화면 수가 많은 Devths에서 구조를 일관되게 유지
      • 페이지 성격에 맞게 SSG/CSR을 선택해 초기 로딩 UX(랜딩)와 인터랙션 중심 화면(채팅/캘린더)을 균형 있게 구성할 수 있음
  • 상태 관리
    • Zustand
      • 전역 UI 상태(모달/토스트/로딩/드로어 등)를 store로 단순하게 관리
      • 필요한 상태만 선택 구독하여 불필요한 리렌더링 최소화
  • UI 컴포넌트
    • shadcn/ui
      • Tailwind 기반으로 컴포넌트를 빠르게 구성 + 커스터마이징 가능
      • 공통 UI를 표준화(버튼/다이얼로그/토스트/폼)를 팀 규칙으로 강제하기 좋음
  • 스타일링
    • Tailwind CSS
      • 화면이 많아도 클래스 기반으로 빠르게 적용 가능
      • 컴포넌트 단위 응집도 높음
  • 폼 관리
    • React Hook Form + Zod
      • 폼이 많은 서비스이기에 입력 상태/검증/에러 표시 표준화 필요성
      • Zod 스키마로 검증 규칙을 한 곳에 모아 타입과 검증의 불일치를 줄일 수 있음
  • 데이터 페칭
    • TanStack Query
      • 서버 데이터가 많은 화면에서 캐시 기반으로 중복 요청을 줄이고, 로딩/에러/성공 상태를 일관되게 관리 가능
  • API 통신
    • Axios
      • baseURL/헤더/인터셉터를 한 곳에서 표준화해 API 호출 방식이 화면마다 달라지는 것을 방지함
  • 실시간
    • STMOP + WebSocket
      • Pub/Sub 기반이라 1:1/그룹 채팅, 읽음 처리, 입장/퇴장 이벤트 확장에 유리
      • Spring 기반 서버와 궁합이 좋음
  • CSR/SSG 적용 범위
    • / (랜딩) : SSG
      • 정적 콘텐츠 중심 → 빌드 시 HTML 생성 → 빠른 초기 로딩
    • /auth/* (OAuth 콜백) : CSR
      • 사용자마다 매번 다른 인가 코드/쿼리를 받아 처리해야 하므로 정적 생성 불가
      • 브라우저에서 쿼리 파싱 → 서버 토큰 교환 API 호출 → 로그인 상태 설정 → 리다이렉트 흐름이 자연스러움
    • /signup : CSR
      • 입력/업로드가 핵심인 폼 화면
    • /calendar, /board, /chat, /profile 등 메인 기능 페이지 : CSR
      • 로그인 이후 사용자 개인화 데이터(일정/글/채팅/알림) 기반 + 필터/무한스크롤/실시간 업데이트 등 클라이언트 상태 변화가 많음

주요 페이지 / 컴포넌트 구조

적용 범위

  • 공개 영역(로그인 전): 랜딩 /, 로그인 /login, 회원가입 /signup, OAuth 콜백 /auth/callback
  • 보호 영역(로그인 후): /calendar, /board, /chat, /llm, /notifications, /profile

라우팅/레이아웃 구조

  • 라우팅 그룹
    • app/(public)/ : 랜딩 /, 로그인 /login, 회원가입 /signup, OAuth 콜백 /auth/callback
    • app/(app)/ : 보호 페이지 /calendar, /board, /chat, /llm, /notifications, /profile
  • 공통 레이아웃(AppShell) 적용 구간
    • app/(app)/layout.tsx 에서 공통 레이아웃 + 전역 Provider + 인증 가드(AuthGate)를 한 번에 적용
  • AppShell에서 하는 일
    • 공통 UI 레이아웃
      • Header (상단 고정: Devths 로고 + 알림 아이콘)
      • BottomNav (하단 고정: 홈/피드/AI/채팅/프로필)
      • main 컨텐츠 영역(페이지별 children 렌더링)
    • Provider 장착
      • QueryClientProvider (TanStack Query)
      • Toaster (토스트를 쓰는 경우에만)
    • 전역 UI Host
      • GlobalLoadingHost (전역 로딩 모달)
      • GlobalInfoModalHost (전역 안내 모달)
      • GlobalConfirmModalHost (전역 확인 모달)
    • AuthGate
      • 보호 페이지 접근 시 로그인 상태 확인
      • 미로그인/만료 상태면 로그인 페이지 이동

(public) 로그인 페이지 /login

  • 주요 기능

    • 서비스 소개 슬라이드(온보딩)
      • 최대 5개 슬라이드
      • 사용자가 스와이프해서 넘길 수 있음
      • 자동 재생: 5초마다 다음 슬라이드로 이동(마지막 슬라이드 후 1번으로 순환)
      • 현재 위치를 도트 네비게이션으로 표시(도트 클릭 시 해당 슬라이드로 이동)
    • 구글 OAuth로그인 시작
      • Sign in with Google 버튼 클릭 시 OAuth 플로우 시작
      • OAuth 성공 시 /auth/callback로 이동
    • 로그인 실패 안내
      • OAuth 실패/취소/네트워크 오류 시 전역 InfoModal로 안내
      • 로그인 처리 시간이 길어질 경우 전역 LoadingModal로 ‘로그인 처리 중’ 표시
  • 사용 컴포넌트

    • 페이지 구성 컴포넌트
      • OnboardingCarousel (슬라이드 영역)
      • DotPagination (도트 네비게이션)
      • GoogleLoginButton (구글 로그인 버튼 UI)
    • 공통 컴포넌트 (재사용)
      • InfoModal (전역)
      • LoadingModal (로그인 요청이 길어질 경우 “로그인 처리 중” 표시)
  • 데이터 로딩 시점

    • 최초 진입 시 별도 API 호출 없음
    • 버튼 클릭 시에만 OAuth 시작
      • /api/auth/google 호출은 /auth/callback 페이지에서 수행(인가 코드가 그때 생김)
  • 라우팅

    • /login

      → (사용자가 Google 로그인 클릭)

      → Google OAuth 페이지

      → (성공) /auth/callback?code=...

      /api/auth/google로 code 교환

      → (회원) /calendar

      → (신규) /signup

  • 내부 상태 & 이벤트

    • currentIndex (현재 슬라이드 인덱스)
    • 자동 재생 타이머(5초): 페이지 이탈/언마운트 시 clear
    • 스와이프/도트 클릭 시 currentIndex 업데이트
    • 로그인 버튼 클릭 시 OAuth 리다이렉트 시작
  • 에러 처리 / 엣지 케이스

    • 이미 로그인 상태로 /login 접근
      • UX를 위해 /calendar로 리다이렉트 처리
    • 네트워크 오류/서버 오류(5xx)
      • InfoModal: 로그인에 실패했습니다. 잠시 후 다시 시도해 주세요

(app) 보호 페이지 공통

  • 주요 기능
    • 헤더/하단탭 유지
      • 상단 Header(로고 + 알림 아이콘) 고정
      • 하단 BottomNav(홈/피드/AI/채팅/프로필) 고정
    • main(children) 영역에서 각 도메인 페이지(캘린더/게시판/채팅/AI/알림/프로필) 렌더링
    • 전역 UX 일관성 유지
      • 도메인과 상관없이 로딩/안내/확인은 전역 모달로 통일
        • 긴 작업: LoadingModal
        • 결과 안내: InfoModal
        • 사용자 선택 필요: ConfirmModal
  • 사용 컴포넌트
    • 공통 레이아웃 컴포넌트
      • Header, BottomNav
    • 전역 UI Host (AppShell에서 1회 렌더)
      • GlobalLoadingHostLoadingModal
      • GlobalInfoModalHostInfoModal
      • GlobalConfirmModalHostConfirmModal
  • 데이터 로딩 시점
    • AppShell 진입 시 AuthGate에서 내 정보 조회로 로그인 여부 판단
      • 기준 API: GET /api/users/me
    • 성공 시
      • 보호 페이지 렌더링 허용
    • 실패 시(미로그인/만료)
      • /login으로 리다이렉트
      • 이동 전에 InfoModal로 ‘로그인이 필요합니다’ 안내 후 이동
  • 라우팅
    • 보호 영역 페이지
      • /calendar, /board, /chat, /llm, /notifications, /profile
  • 내부 상태 & 이벤트
    • AuthGate 상태
      • isCheckingAuth (인증 확인 중)
      • isAuthed (로그인 여부)
  • 에러 처리 / 엣지 케이스
    • 전역 모달 동시 표시 방지 규칙
      • LoadingModal 활성 중에는 Info/Confirm을 동시에 띄우지 않고
      • 로딩 종료 후 결과를 InfoModal로 안내하는 흐름으로 통일
    • 네트워크 오류/서버 오류(5xx)
      • 일시 오류로 판단: InfoModal로 안내 + 재시도 유도

(app) 에러 페이지

  • 공통 정책
    • Header / BottomNav 유지
    • main 영역에만 에러 안내 UI 표시
    • 공통 CTA 버튼 제공: 메인으로 돌아가기
      • 클릭 시 캘린더 페이지(메인) 로 이동: router.replace("/calendar")
  • 404: app/(app)/not-found.tsx
    • 노출 조건
      • 존재하지 않는 경로로 접근했을 때 자동 렌더
    • 표시 내용
      • 타이틀: 404 Not Found
      • 안내 문구: 존재하지 않는 페이지입니다. URL을 확인해주세요!
      • 버튼: 메인으로 돌아가기/calendar
    • 엣지 케이스
      • 공유 URL/딥링크로 잘못 들어온 경우도 이 화면에서 복구 가능해야 함(버튼으로 메인 복귀)
  • 500: app/(app)/error.tsx
    • 노출 조건
      • 런타임 에러(렌더링 오류 등) 발생 시 자동 렌더
    • 표시 내용
      • 타이틀: 500 Server Error
      • 안내 문구: 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요!
      • 버튼: 메인으로 돌아가기/calendar
    • 로깅
      • error.tsx에서 console.error(error)로 기본 기록

주요 컴포넌트

(1) Header

  • 개요 (Overview)
    • 역할
      • 보호 영역 모든 화면 상단 고정 헤더
      • 로고/알림은 공통
      • 검색은 페이지별로 옵션 제공(노출/동작이 다름)
    • 디자인 시안
      • 좌측 ‘Devths’ 텍스트(로고)
      • 우측: (조건부)검색 아이콘, 알림(벨) 아이콘
        • 읽지 않은 알림 뱃지
  • props & Interface
    • showSearch?: boolean (기본 false)
      • true일 때만 검색 아이콘 노출
    • onClickSearch?: () => void
      • 검색 아이콘 클릭 시 실행할 동작(페이지가 주입)
    • searchHref?: string
      • 클릭 시 이동할 경로를 문자열로 전달(페이지가 주입)
  • 내부 상태 & 이벤트
    • 상태
      • hasUnread: boolean (읽지 않은 알림 존재 여부)
    • 이벤트
      • 로고 클릭: /calendar 이동
      • 검색 아이콘 클릭:
        • onClickSearch가 있으면 실행
        • 없고 searchHref가 있으면 해당 경로로 이동
      • 알림 아이콘 클릭: /notifications 이동
  • 사용 예시
    • AppShell에서 기본 렌더(검색 없음)
      • <Header showSearch={false} />
    • 게시판 페이지(게시글 검색)
      • <Header showSearch searchHref="/board/search" />
    • 채팅 페이지(채팅 검색)
      • <Header showSearch onClickSearch={() => router.push("/chat/search")} />
  • Storybook
    • Default: 검색 없음, 뱃지 없음
    • Unread: 검색 없음, 읽지 않은 알림 뱃지 있음
    • SearchOnly: 검색 있음, 뱃지 없음
    • SearchWithUnread: 검색 있음, 읽지 않은 알림 뱃지 있음
    • SearchOnly(Board), SearchOnly(Chat) : 검색 클릭 동작이 페이지별로 다른 케이스 검증

(2) BottomNav

  • 개요

    • 역할
      • 보호 영역 모든 화면 하단 고정 탭 네비게이션
      • 주요 도메인(홈/피드/AI/채팅/프로필)으로 빠른 이동 제공
    • 디자인 시안
      • 아이콘 + 라벨이 있는 5개 탭
      • , 피드, AI, 채팅, 프로필
  • props & Interface

    • 기본은 props 없이 사용
    • 현재 경로(pathname) 기준으로 active 탭을 자동 표시
  • 탭 구성/라우팅 매핑

    • /calendar
    • 피드/board
    • AI/llm
    • 채팅/chat
    • 프로필/profile

    탭 이동은 router.push()로 통일

  • 내부 상태 & 이벤트

    • 상태
      • 별도 local state 없이 usePathname() 기반으로 active 판단
    • 이벤트
      • 탭 클릭 → 해당 경로로 이동
    • active 판정 규칙
      • 정확 매칭 + 하위 경로 포함 처리
        • /board 또는 /board/*피드 active
        • /chat 또는 /chat/*채팅 active
  • Storybook

    • 탭 활성 상태 조합 케이스
      • HomeActive
      • BoardActive
      • LlmActive
      • ChatActive
      • ProfileActive

(3) ConfirmModal (전역)

  • 개요
    • 역할
      • 사용자의 선택(취소/확인)이 필요한 상황에서 사용하는 전역 확인 모달
      • 작성 중 이탈, 삭제, 로그아웃, 권한 요청 등
    • 디자인 시안
      • 제목/설명 + 우측 상단 X
      • 하단 버튼 2개: 취소 / 확인
      • 확인 버튼 텍스트는 상황별로 변경(나가기/삭제/로그아웃 등)
      • 파괴적 액션은 confirm 버튼을 destructive 스타일로 표시
  • props
    • title: string
      • 모달 맨 위에 크게 보이는 제목
      • 예: "작성 중인 내용이 있습니다"
    • description?: string
      • 제목 아래에 들어가는 설명 문구(부가 안내)
      • ?라서 없어도 됨
      • 예: "저장하지 않고 나가시겠습니까?"
    • cancelText?: string
      • 왼쪽(보통 회색) 취소 버튼에 표시될 글자
      • 안 주면 기본으로 "취소"가 뜸
      • 예: "아니요"로 바꾸고 싶으면 여기서 설정
    • confirmText?: string (기본: "확인")
      • 오른쪽(보통 강조) 확인 버튼에 표시될 글자
      • 상황마다 "나가기", "삭제", "로그아웃"처럼 바꾸려고 쓰는 값
    • confirmVariant?: "default" | "destructive" (기본: default)
      • 확인 버튼의 성격(스타일)을 정함
      • "default": 일반 확인(예: 저장, 진행)
      • "destructive": 위험/삭제/되돌릴 수 없는 행동(예: 삭제, 탈퇴) → 빨간 계열 스타일
    • onConfirm: () => void | Promise<void>
      • 사용자가 확인 버튼을 눌렀을 때 실행할 함수
        • 글 작성 중 나가기 → router.back()
        • 삭제 → await deletePost()
  • 내부 상태 & 이벤트
    • 상태
      • isOpen: boolean
        • 모달이 열려있는지/닫혀있는지를 나타내는 값
        • true면 ConfirmModal이 화면에 보이고,
        • false면 안 보임
        • 전역 store(Zustand)가 이 값을 관리해서, 어느 페이지에서든 opne/close를 동일한 방식으로 제어 가능
      • isSubmitting: boolean
        • 사용자가 확인 버튼을 눌렀고, onConfirm 작업이 진행 중인지를 나타내는 값
        • true일 때
          • 확인 버튼을 disabled 처리
          • 버튼에 로딩 표시
          • 연타로 API가 여러 번 호출되는 것을 방지함
        • onConfirm이 끝나면 false로 돌아감
          • 성공이면 모달 닫기
          • 실패면 모달 유지 + 에러 안내
    • 이벤트
      • 확인 버튼 클릭
        • isSubmitting=true 로 설정 (연타 방지)
        • await onConfirm() 실행
        • 성공
          • closeConfirm() (모달 닫기)
        • 실패
          • 모달 유지
          • InfoModal로 에러 안내
        • 마지막
          • 성공/실패와 무관하게 isSubmitting=false로 복구
      • 취소 버튼 클릭
        • onCancel이 있으면 실행
        • closeConfirm()로 모달 닫기
      • X 클릭
        • closeConfirm()
  • 사용 예시
    • 작성 중 이탈
      • 상황: 글 작성/수정 중 뒤로 가기, 탭 이동, 페이지 이탈 시
      • 설정
        • confirmText = "나가기"
        • confirmVariant = "default"
      • 동작 예시(onConfirm)
        • 작성 페이지 이탈 처리: router.replace("/board")
    • 삭제 확인
      • 상황: 게시글/댓글/파일 삭제 버튼 클릭 시
      • 설정
        • confirmText = "삭제"
        • confirmVariant = "destructive"
      • 동작 예시(onConfirm)
        • 삭제 API 호출: await deletePost(postId)
        • 성공 후 목록 갱신: queryClient.invalidateQueries(...)
    • 로그아웃
      • 상황: 프로필화면에서 로그아웃 버튼 클릭 시
      • 설정
        • confirmText = "로그아웃"
        • confirmVariant = "destructive"
      • 동작 예시(onConfirm)
        • await api.post("/api/auth/logout")
        • authStore.logout() (토큰/상태 초기화)
        • router.replace("/login")
  • 전역 관리
    • Zustand store로 단일화
      • 페이지/컴포넌트 어디서든 동일한 API로 모달 제어
        • useConfirmStore.openConfirm(payload)
          • 모달 열기(표시 내용/동작 주입)
        • useConfirmStore.closeConfirm()
          • 모달 닫기
    • 렌더링 위치
      • GlobalConfirmModalHost를 AppShell에서 1회만 렌더
      • 각 페이지는 ConfirmModal을 직접 렌더하지 않고, openConfirm만 호출
        • 장점: 모달이 중복 렌더되지 않고, UI/동작 규칙이 한 곳에서 통일됨
    • 책임 분리
      • 페이지: 언제 띄울지 + 어떤 문구/액션인지(payload)만 결정
      • Host: 어떻게 보여줄지(레이아웃/닫힘 규칙/제스처/스크롤 락)를 담당

(4) LoadingModal (전역)

  • 개요
    • 역할
      • 시간이 걸리는 작업 진행 중 사용자에게 처리 중 상태를 보여주는 UI
      • 작업 성격에 따라 Blocking / Non-blocking으로 구분하여 사용
        • Blocking(화면 잠금): 사용자가 현재 플로우를 끝내야 하는 짧은 작업(저장/제출/필수 업로드 등)
        • Non-blocking(백그라운드 작업): OCR/AI 마스킹처럼 오래 걸리고 사용자가 다른 페이지로 이동할 수 있어야 하는 작업
    • 디자인 시안: 딤 + 카드 + 문구 + 스피너
  • props
    • title: string
      • 로딩 제목(예: “AI가 개인정보를 마스킹하는 중입니다”)
    • description?: string
      • 보조 설명(예: “잠시만 기다려 주세요”)
    • showSpinner?: boolean (기본 true)
      • 스피너 표시 여부
    • blockClose?: boolean (기본 false)
      • 사용자 화면 이동/닫기 가능 여부
      • true: 모달을 닫을 수 없고(overlay/ESC 포함), 사실상 화면을 잠금(Blocking)
      • false: 사용자가 닫거나 이동 가능(Non-blocking 용도)
  • 내부 상태 & 이벤트
    • 상태
      • isOpen: boolean (전역 store에서 제어)
    • 규칙
      • blockClose=true일 때
        • X 버튼 숨김/비활성화
        • overlay 클릭/ESC로 닫기 불가
        • 페이지 이탈도 제한
      • blockClose=false일 때
        • X/overlay/ESC로 닫기 가능
        • 사용자는 다른 페이지로 이동 가능(백그라운드 진행)
  • 전역 관리
    • Zustand store로 단일화
      • openLoading(payload) : 로딩 열기
      • updateLoading(payload) : 문구/옵션 업데이트(진행 단계 문구 변경)
      • closeLoading() : 로딩 닫기
  • 사용 정책
    • Blocking Loading(잠금 필요)
      • blockClose=true
      • 예: 저장 중, 제출 중, 필수 업로드 중
    • Background 작업(이동 가능)
      • blockClose=false + 완료/실패는 InfoModal 또는 /notifications로 안내
      • 예: OCR/AI 마스킹/분석 task 폴링

(5) InfoModal (전역)

  • 개요
    • 역할
      • 성공/안내/경고/오류 등 사용자에게 결과를 알려주고 사용자가 닫는 전역 모달
      • 로딩을 제외한 대부분의 결과 안내는 InfoModal로 통일
    • 디자인 시안
      • 제목 + 설명 + 우측 상단 X(닫기)
      • 확인 버튼 없이 X로 닫는 형태
  • Props & Interface
    • title: string
      • 모달에서 가장 크게 보이는 제목 텍스트
      • 예: 저장이 완료되었습니다
    • description?: string
      • 제목 아래에 붙는 추가 설명 문구
      • 없어도 됨(?)
      • 예: 변경사항이 정상적으로 저장됐어요.
    • variant?: "success" | "info" | "warning" | "error" (기본 "info")
      • 모달의 상태를 나타내는 값
      • 아이콘/색/강조 스타일을 바꾸는 데 사용
      • "success": 성공(예: 저장 완료, 업로드 완료)
      • "info": 일반 안내(예: 로그인이 필요합니다)
      • "warning": 주의(예: 중요 정보가 포함될 수 있어요)
      • "error": 오류(예: 요청에 실패했습니다)
    • onClose?: () => void
      • 사용자가 X를 눌러서 모달을 닫을 때 추가로 실행할 함수
      • 예: 모달 닫은 뒤 특정 페이지로 이동시키고 싶을 때
        • "로그인이 필요합니다" 모달 닫으면 /login으로 이동 같은 처리
  • 내부 상태 & 이벤트
    • isOpen: boolean (전역 store에서 제어)
      • InfoModal이 지금 화면에 떠있는지 여부
      • true면 모달이 보이고, false면 안 보임
      • 전역 store가 관리하니까 어디서든 openInfo()로 열 수 있음
    • X 클릭 → closeInfo()
      • 사용자가 우측 상단 X 버튼을 누르면 모달을 닫는 함수 실행
    • overlay / ESC 닫기 허용
      • overlay: 모달 밖의 어두운 배경
        • 배경을 클릭해도 모달이 닫히도록 허용한다는 뜻
      • ESC : 키보드 ESC 키
        • ESC를 눌러도 모달이 닫히도록 허용한다는 뜻
  • 사용 예시
    • 파일 첨부 성공: 첨부가 완료되었습니다
    • 저장 완료: 저장이 완료되었습니다
    • 권한 안내: 로그인이 필요합니다
    • 네트워크/서버 오류: 일시적인 오류가 발생했습니다
  • 전역 관리
    • Zustand store로 단일화
      • openInfo({ title, description, ... })
      • closeInfo()
    • 렌더링 위치
      • GlobalInfoModalHost를 AppShell에서 1회만 렌더
      • 각 페이지는 열기(open)만 호출하고 실제 렌더는 Host가 담당
  • Storybook
    • Info: 기본 안내
    • Success: 성공 안내
    • Warning: 경고 안내
    • Error: 오류 안내
    • LongText: 긴 설명/줄바꿈 케이스

(6) NoticeBanner

  • 개요
    • 역할
      • 채팅/게시글/댓글 작성 등 사용자 입력 화면에서 보안 안내를 항상 고정 노출하는 배너
      • 사용자가 작성 중에도 개인정보 공유 주의를 지속적으로 인지하도록 함
    • 디자인 시안
      • 좌측: 원형 느낌의 주의 아이콘 (!)
      • 우측: 텍스트 영역
        • 1줄: 제목(보안 안내)
        • 2줄: 안내 문구(연락처, 계좌번호, 주민번호 등 개인정보를 공유하지 마세요)
  • Props & Interface
    • title: string
      • 배너 제목(보안 안내)
    • message: string
      • 안내 문구
    • icon?: ReactNode
      • 기본은 Info/Alert 아이콘을 사용
  • 내부 상태 & 이벤트
    • 닫기 버튼 없음
    • overlay/클릭/ESC 등으로 숨김 처리 없음
    • 페이지가 렌더링되는 동안 항상 표시
  • 사용 예시
    • 채팅 작성 상단 고정
      • title="보안 안내"
      • message="연락처, 계좌번호, 주민번호 등 개인정보를 공유하지 마세요"
    • 게시글/댓글 작성 화면 상단 고정(동일 문구 재사용)
  • Storybook
    • Default(Security): 기본 보안 안내 배너(항상 노출)
    • LongText: 긴 문구 줄바꿈 케이스

(7) ChipTabs

  • 개요
    • 역할
      • 카테고리/필터 선택을 위한 칩 형태 탭 UI
      • 도메인별 사용 예
        • 게시글: 이력서/포트폴리오/면접/코딩테스트
        • 프로필: 백엔드/프론트엔드/클라우드/AI
        • 캘린더: 서류/코테/1차 면접/2차 면접/개인 일정
    • 디자인
      • pill 버튼 가로 나열
      • 선택된 탭만 검정 배경 + 흰 글자
      • 미선택 탭은 연한 배경/테두리
  • Props & Interface
    • items: { value: string; label: string }[]
      • 탭 목록(값/표시 라벨)
    • value: string
      • 현재 선택된 탭의 value (controlled)
    • onChange: (nextValue: string) => void
      • 탭 클릭 시 부모에게 선택값 변경 요청
  • 내부 상태
    • 내부 상태 없음
      • 선택 상태는 항상 value로 판단
    • 이벤트
      • 탭 클릭 → onChange(item.value) 호출
  • 사용 예시
    • 게시글 카테고리 필터
    • items=[{value:"resume",label:"이력서"}, ...]
    • value는 URL 쿼리 또는 로컬 state로 관리
    • 변경 시 목록 쿼리 refetch 또는 필터 적용
  • 엣지 케이스
    • 탭이 많아 한 줄에 다 안들어가면
      • 컨테이너에 overflow-x-auto + whitespace-nowrap
      • 스크롤 시 레이아웃 깨지지 않게 gap 유지
    • items가 비어있으면
      • 렌더하지 않음
    • value가 items에 없는 경우
      • 첫 번째 탭을 기본값으로 강제
  • Storybook
    • Default: 4개 탭 기본
    • Active: 특정 탭 active 상태
    • Overflow: 탭이 많은 가로 스크롤 케이스
    • LongLabel: 라벨이 긴 케이스

(8) Avatar

  • 개요
    • 역할
      • 사용자 프로필을 원형으로 표시
      • src가 있으면 이미지 표시
      • 이미지가 없거나 실패하면 fallback 표시
        • 기본: 닉네임(name)의 앞글자 1개
        • name도 없으면 기본 원형(placeholder)
      • 프로필 미등록 상태는 특정 기본 색상 배경 + 이니셜로 표시
  • Props & Interface
    • src?: string
      • 프로필 이미지 URL(또는 미리보기 URL)
    • name?: string
      • fallback 이니셜 생성용 닉네임/이름
    • size?: "xs" | "sm" | "md" | "lg" | "xl" (기본 "md")
      • 아바타 크기 프리셋
    • Avatar의 fallback 배경색은 페이지/상황에 따라 분기하지 않고, 디자인 토큰 avatar-fallback으로 단일화
  • 내부 상태 & 이벤트
    • 내부 상태: hasError: boolean
      • 이미지 로드 실패 시 true로 전환 → 이후 fallback로 고정
    • 이벤트
      • 이미지 onError 발생 → hasError=true
  • Fallback 규칙
    • src가 있고 로드 성공 → 이미지 표시
    • src가 없거나 로드 실패 + name 있음 → name의 첫 글자 표시
    • src 없고 name도 없음 → placeholder 원형 표시
  • 엣지 케이스
    • name이 빈 문자열/공백이면 이니셜 대신 placeholder 처리
    • 이미지가 깨지거나 404면 onError로 fallback 전환
    • 원형 크롭 유지: object-fit: cover 적용

(9) DevthsButton (Primary Button)

  • 개요
    • 역할
      • 서비스에서 주요 행동을 수행하는 버튼
      • 추가/저장/확인/제출/로그인
    • 디자인 시안: 검정 배경 + 흰 글자, 둥근 사각형
  • Props & Interface
    • 콘텐츠
      • label?: string
        • 버튼에 보여줄 문자열 텍스트
        • 예: "저장", "확인"
    • 동작/상태
      • onClick?: () => void
        • 버튼을 눌렀을 때 실행할 함수(이벤트)
        • 예시: 저장 API 호출, 페이지 이동 등
      • disabled?: boolean (기본 false)
        • true면 버튼이 비활성화(회색) 되고 클릭이 안 됨
        • 예: 입력값이 아직 유효하지 않을 때
      • isLoading?: boolean (기본 false)
        • true면 지금 처리 중 상태
          • 버튼에 로딩 표시(스피너)
          • 버튼 클릭 불가 처리(연타 방지)
        • 예: 저장 버튼 눌렀는데 서버 응답 기다리는 중
    • type?: "button" | "submit" (폼에서 필요)
      • HTML 버튼의 타입
      • "button": 그냥 클릭용(기본값으로 많이 둠)
      • "submit": 폼 안에서 누르면 폼 제출(onSubmit) 을 트리거함
      • 예: 회원가입 폼 “가입하기” 버튼은 type="submit"이 자연스러움
  • 상태 규칙
    • Enabled(활성)
      • 배경: 검정
      • 텍스트: 흰색
    • Disabled(비활성)
      • 배경: 회색
      • 클릭 불가 (disabled=true)
      • 포커스/호버 효과 최소화
    • Loading(로딩)
      • isLoading=true일 때
        • 버튼 비활성화 처리(연타 방지)
        • 스피너(또는 로딩 아이콘) 표시
  • 사용 예시
    • 저장 버튼: label="저장"
  • Storybook
    • Default: enabled
    • Disabled: disabled=true(회색)
    • Loading: isLoading=true
    • LongText: 라벨이 긴 케이스
    • Submit: type="submit" 폼 버튼 케이스

컴포넌트 간 관계

  • AppShell (app/(app)/layout.tsx)
    • 보호 영역 공통 레이아웃을 제공하며, 아래 요소들을 항상 고정 렌더링한다.
      • 상단: Header
      • 중앙: main(children) → 각 페이지 컴포넌트가 이 영역에 렌더링됨
      • 하단: BottomNav
    • 전역 UI는 AppShell에서 단 한 번만 렌더링한다(중복 렌더 방지).
      • GlobalLoadingHostLoadingModal
      • GlobalInfoModalHostInfoModal
      • GlobalConfirmModalHostConfirmModal
  • 페이지(Page) 컴포넌트의 역할
    • 페이지는 모달 UI를 직접 렌더링하지 않고, 전역 store 호출로만 제어한다.
      • 로딩 표시: useLoadingStore.openLoading() / updateLoading() / closeLoading()
      • 안내 모달: useInfoModalStore.openInfo() / closeInfo()
      • 확인 모달: useConfirmStore.openConfirm() / closeConfirm()
  • 데이터 흐름(요약)
    • Page → store에 열기/닫기/문구를 업데이트
    • Host → store 상태를 구독하여 실제 모달을 렌더링/숨김 처리

상태 관리 전략 (State Management Strategy)

  • Global State (Zustand Stores)
    • 목적: 서버 데이터가 아닌 전역 UI 상태와 세션/인증 상태 관리
    • 사용 기준
      • 여러 페이지에서 공통으로 필요하거나 페이지 이동해도 유지되어야 하는 상태 = Zustand
      • 서버에서 내려오는 데이터(목록/상세)는 TanStack Query로 관리
    • Store
      • useAuthStore
        • accessToken, isLoggedIn, logout 등 인증/세션 상태
      • useLoadingStore
        • 전역 로딩 모달 isOpen, title, description, blockClose
      • useInfoModalStore
        • 전역 안내 모달 isOpen, title, description, variant
      • useConfirmStore
        • 전역 확인 모달 isOpen, payload(title/description/confirmText/onConfirm...)
      • useNavStore
        • 현재 선택된 하단 탭 상태
  • Local State (React useState, useReducer)
    • 목적
      • 특정 페이지/컴포넌트 내부에서만 쓰이는 일시적인 UI 상태 관리
    • 사용 예
      • 입력값, 탭 선택값(ChipTabs), 드롭다운 오픈 여부, 모달 내부 임시 값, 선택 리스트 등
    • 사용 기준(룰)
      • 그 화면을 벗어나면 의미 없어지는 상태는 Local State
  • Server Cache State (TanStack Query)
    • 목적
      • 서버에서 가져오는 데이터를 캐싱/동기화하고, 로딩/에러/성공 상태를 표준화
      • 무한 스크롤/페이지네이션/리패치 정책을 일관되게 적용
    • 사용 기준(룰)
      • API에서 오는 데이터는 React Query가 단일 소스(Zustand 중복 저장 금지)
    • 권장 패턴
      • GET /api/users/me 같은 전역 기준 데이터(내 정보)는 AppShell(AuthGate)에서 1회 조회 후 재사용
      • 알림/목록/검색은 화면 특성에 따라 staleTime, refetchOnWindowFocus, useInfiniteQuery 등을 정책으로 설정
      • 변경(작성/삭제/수정) 이후에는 invalidateQueries로 목록/상세를 갱신

API 연동 (API Integration)

(1) 호출할 백엔드 API 목록

  • 인증
    • POST /api/auth/google
      • OAuth 인가 코드(code/authCode)를 서버에 전달 → 회원 여부 판단 + 토큰 발급
      • 사용 위치: /auth/callback에서 호출
    • POST /api/auth/tokens
      • refreshToken 기반 accessToken 재발급
      • 사용 위치: axios 응답 인터셉터의 401 자동 갱신에서만 사용(직접 호출 최소화)
    • POST /api/auth/logout
      • 로그아웃 처리(토큰 무효화/쿠키 삭제)
      • 사용 위치: 프로필/설정 화면 로그아웃 액션
  • 유저 (Users)
    • GET /api/users/me
      • 로그인 상태 판별 기준 API
      • 사용 위치: AppShell AuthGate에서 최초 실행
    • PUT /api/users/me
      • 내 정보 수정(프로필 이미지/닉네임/관심사 등)
      • 사용 위치: 프로필 편집 폼 제출
    • DELETE /api/users
      • 회원 탈퇴
      • 사용 위치: 설정/탈퇴 확인(ConfirmModal) 이후 실행
    • (프로필/팔로우 관련)
      • GET /api/users/{userId}
        • 특정 유저 프로필 조회
      • POST /api/users/{userId}/followers
        • 팔로우
      • DELETE /api/users/{userId}/followers
        • 언팔로우
      • GET /api/users/me/followers?size=&lastId=
        • 내 팔로워 목록(무한스크롤)
      • GET /api/users/me/followings?size=&lastId=&nickname=
        • 내 팔로잉 목록(무한스크롤)
      • GET /api/users/me/posts?size=&lastId=
        • 내가 쓴 글 목록(무한스크롤)
      • GET /api/users/me/comments?size=&lastId=
        • 내가 쓴 댓글 목록(무한스크롤)
  • 알림(Notifications)
    • GET /api/notifications?size=&lastId=
      • 알림 목록 조회(무한스크롤/페이지네이션)
      • 사용 위치: /notifications 페이지, Header 뱃지 표시용 최신 상태 확인
    • 디바이스 토큰 관리(푸시/디바이스 등록용)
      • POST /api/notifications/tokens/{deviceId} : 등록
      • PATCH /api/notifications/tokens/{deviceId} : 갱신
      • DELETE /api/notifications/tokens/{deviceId} : 삭제 (로그아웃/탈퇴 시)
  • 파일 첨부
    • POST /api/files/presigned
      • S3 업로드용 Presigned URL 발급
      • 사용 위치: 업로드 시작 단계
    • POST /api/files
      • 업로드 완료 후 서버에 파일 메타 등록(원본명, s3Key 등)
      • 사용 위치: S3 업로드 성공 직후
    • DELETE /api/files/{fileId}
      • 첨부 파일 삭제(서버 메타/권한 처리)
  • 비동기 작업 상태 조회
    • GET /api/ai/tasks/{taskId}
      • AI/OCR/마스킹 등 비동기 작업 상태 조회
      • 사용 위치: 폴링(예: 1~2초 간격)로 진행/완료/실패 확인
      • 완료/실패 시: Loading 종료 + InfoModal/알림으로 결과 안내

(2) API 호출 처리

  • axios 인스턴스 단일화
    • apiClient = axios.create({ baseURL, withCredentials: true })
      • withCredentials: true → refreshToken이 쿠키라면 필요
    • 모든 API 호출은 apiClient로만 수행(직접 axios 금지)
  • Request Interceptor (요청 전)
    • Zustand의 authStore.accessToken을 읽어서
      • 있으면 Authorization: Bearer <accessToken> 자동 첨부
    • Content-Type은 기본 application/json
      • 파일은 Presigned 방식이므로 백엔드로 직접 멀티파트 업로드하지 않음(원칙)
  • Response Interceptor (응답 후) : 401 자동 갱신 표준
    • 응답이 401이면 아래 순서로 처리
      1. POST /api/auth/tokens 호출로 accessToken 재발급 시도
      2. 성공하면: 새 accessToken 저장(Zustand) → 원래 요청 1회 재시도
      3. 실패하면: authStore 초기화 + InfoModal로 안내 후 /login 이동
    • 동시 401 폭주 방지
      • refresh 요청은 한 번만 수행
      • 나머지 실패 요청은 refresh 완료 후 재시도하도록 큐/락 적용
  • 공통 UX 처리(모달 정책)
    • LoadingModal 사용 정책을 2가지로 구분
      • Blocking(화면 잠금): 사용자가 지금 화면에서 반드시 기다려야 하는 짧은 작업
        • 예: 저장/제출/필수 업로드 완료 전 단계
        • useLoadingStore.openLoading({ title, description, blockClose: true })
      • Non-blocking(백그라운드 작업): 시간이 길어 다른 페이지 이동이 가능해야 하는 작업
        • 예: OCR/AI 마스킹/분석 task 폴링
        • useLoadingStore.openLoading({ title, description, blockClose: false }) 또는 LoadingModal 대신 배너/알림으로 대체(정책 선택)
    • 작업 단계 문구 변경
      • updateLoading({ title?, description?, showSpinner?, blockClose? })
    • 완료/실패 시
      • closeLoading() 후 결과 안내는 InfoModal로 통일
        • useInfoModalStore.openInfo({ title, description })
    • 사용자 선택이 필요한 경우
      • useConfirmStore.openConfirm({ title, description, confirmText, onConfirm, ... })
  • 파일 첨부 표준 플로우 (Presigned)
    1. POST /api/files/presigned (fileName, mimeType) → presignedUrl/s3Key 수신
    2. 브라우저에서 S3로 직접 PUT presignedUrl 업로드
    3. 업로드 성공 후 POST /api/files로 서버에 파일 메타 등록
    4. 삭제는 DELETE /api/files/{fileId}
  • 비동기 작업(task) 표준 플로우
    • 서버가 taskId를 주는 작업(AI/OCR/마스킹 등)
      • GET /api/ai/tasks/{taskId}를 폴링(예: 1~2초 간격)
      • 진행 중
        • 페이지 이동 필요하면 blockClose=false(Non-blocking) 정책 적용
        • 성공/실패:
          • closeLoading()InfoModal로 결과 안내(또는 다음 화면 이동)
  • 페이지네이션 규칙
    • size, lastId 기반 무한스크롤은 React Query useInfiniteQuery로 통일
    • 다음 페이지 요청 시 lastId는 마지막 아이템의 id를 사용

라우팅 (Routing)

  • Next.js App Router 기반
  • 공개/보호 라우팅을 그룹으로 분리
  • 보호 영역 접근 제어
    • AuthGate에서 로그인 여부 확인 후 보호 페이지 렌더
    • 미로그인 상태면:
      • InfoModal로 안내 후 /login 이동

폼 처리 및 유효성 검증 (Forms & Validation)

  • 유효성 검증 로직
    • 클라이언트 측
      • React Hook Form + Zod로 폼 타입/검증 통일
      • 인라인 에러 메시지 + 제출 버튼 disabled 규칙 통일
      • 제출 중에는 DevthsButton disabled 또는 로딩 처리
    • 서버 측(백엔드)에서도 별도 검증 로직 존재
      • 백엔드에서 400/422 등으로 내려주는 에러는 공통 에러 파서로 메시지 정리 후
        • 필드 에러면 폼 인라인에 연결
        • 일반 에러면 InfoModal로 안내

4️⃣ 이외 고려사항들 (Other Considerations) (Optional)

테스트 전략

  • 단위(Unit)
    • 유틸 함수 중심 테스트
      • 에러 메시지 파서, 날짜 포맷, queryKey 생성 규칙 등
    • 전역 store(Zustand) 로직 테스트
      • open/close/update 같은 상태 전이 테스트
      • ConfirmModalisSubmitting 전환(연타 방지) 같은 로직 포함
  • 컴포넌트(Storybook)
    • 공통 컴포넌트는 상태별 스토리 필수
      • Header
        • 검색 없음/있음
        • 알림 뱃지 없음/있음
        • 검색 있음 + 뱃지 있음 조합
      • BottomNav
        • 각 탭 active 케이스(경로별)
      • InfoModal
        • 제목만 / 제목+설명 / 긴 문장 / 줄바꿈 케이스
      • ConfirmModal
        • confirmText: 나가기/삭제/로그아웃
        • variant: default/destructive
        • submitting 상태(로딩/disabled) 케이스
      • LoadingModal
        • blocking(blockClose=true) 케이스
        • non-blocking(blockClose=false) 케이스
        • 문구 변경(updateLoading) 케이스
      • NoticeBanner
        • 항상 노출(closable 없음)
        • 긴 문구 줄바꿈 케이스
        • (선택) variant가 유지된다면 info/warning/danger 케이스
      • ChipTabs
        • 탭 4개 기본 / 탭 많아서 가로 스크롤 케이스
      • Avatar
        • 이미지 있음 / src 없음 + 이니셜 / 이미지 로드 실패 폴백 / placeholder
      • DevthsButton
        • enabled/disabled(회색)
        • loading(isLoading) 케이스
        • type="submit" 케이스(폼)
  • 통합(Integration)
    • AuthGate 흐름 시나리오 테스트
      • 로그인 상태(true/false)에 따라 보호 페이지 접근 제어가 정상 동작하는지 검증한다.
      • API 응답 401 발생 시, 토큰 재발급(refresh) 성공/실패에 따른 분기 처리가 정상인지 검증한다.
    • API 에러 발생 시 전역 UI 호출 검증
      • 요청 실패 시 전역 로딩 UI가 해제된 후(closeLoading) 안내 모달이 노출되는지(openInfo) 호출 순서를 검증한다.
      • ConfirmModalonConfirm 실행 실패 시 모달을 유지하고, 적절한 에러 안내(InfoModal 등)가 이루어지는지 검증한다.
  • E2E (Playwright)
    • 핵심 사용자 시나리오 위주
      • 보호 페이지 진입 → AuthGate 확인 → 성공 시 메인 진입
      • 401 발생 → refresh 성공 시 정상 복구 / 실패 시 로그인 이동
      • 작성 중 나가기 → ConfirmModal 노출 → 나가기/취소 분기
      • AI 마스킹/분석 → (정책에 따라)
        • blocking이면 LoadingModal 유지 → 완료 후 InfoModal
        • non-blocking이면 다른 페이지 이동 가능 + 완료 후 InfoModal/알림 안내

로깅 및 분석 (Logging & Analytics)

  • 에러 로깅
    • 전역 에러 처리 지점에서 에러 수집
      • Next app/(app)/error.tsx에서 런타임 에러 console.error
      • axios 인터셉터에서 API 에러를 공통 포맷으로 정리하여 로깅
    • 로그에 포함할 정보(개인정보 제외)
      • endpoint, status, errorCode, requestId, userId
  • 성능/UX 모니터링
    • 페이지 진입 시 초기 로딩 시간(내 정보 조회 + 핵심 데이터)
    • TanStack Query 캐시 hit 비율(불필요 refetch 확인)
    • 로딩 모달 노출 시간 분포(너무 길면 UX 개선 필요)
  • 에러 처리 정책
    • HTTP 에러 공통
      • 401: refresh 시도 → 성공 시 원 요청 재시도 → 실패 시 로그아웃 + 로그인 유도(InfoModal 또는 /login)
      • 403: 권한 없음 안내(InfoModal)
      • 404: 리소스 없음 안내(InfoModal 또는 페이지 내 empty state)
      • 5xx: 일시적 서버 오류 안내(InfoModal) + 재시도 버튼 제공
    • 전역 UI 우선 순위
      • LoadingModal이 열려 있으면 다른 모달(Info/Confirm)은 원칙적으로 동시에 띄우지 않음
      • 로딩 종료 후 결과를 InfoModal로 이어서 안내하는 패턴을 기본으로 채택

5️⃣ 용어 정의 (Glossary) (Optional)

  • AppShell
    • 보호 영역에서 공통으로 유지되는 레이아웃( Header + BottomNav + main )과 전역 Provider/Host를 포함한 뼈대
  • AuthGate
    • 보호 페이지 진입 시 로그인 여부를 확인하고, 미로그인일 경우 접근을 막는 컴포넌트/로직
  • Global Host
    • 전역 UI를 딱 한 번만 렌더링하는 컴포넌트
    • 예: GlobalLoadingHost, GlobalInfoModalHost, GlobalConfirmModalHost
  • InfoModal
    • 성공/안내/경고 등을 사용자에게 보여주고 X로 닫는 전역 안내 모달(버튼 없음이 기본)
  • ConfirmModal
    • 사용자의 선택(취소/확인)이 필요한 전역 확인 모달(확인 버튼 텍스트는 상황별 변경)
  • LoadingModal
    • 긴 작업 진행 중 표시하는 전역 로딩 모달(기본적으로 닫기 불가)
  • NoticeBanner
    • 화면 내에 삽입하거나 고정 노출하는 안내 배너(보안 안내 등)
  • Controlled Component
    • 컴포넌트 내부가 아닌 부모가 값을 관리하는 방식(예: ChipTabs의 value/onChange)
  • Server Cache State
    • 서버 데이터를 캐싱/동기화하는 상태(TanStack Query가 담당)
  • Global State
    • 앱 전체에서 공유되는 UI/세션 상태(Zustand가 담당)
⚠️ **GitHub.com Fallback** ⚠️