영상 제작자를 위한 협업 워크스페이스 프론트엔드입니다.
| 구분 | 사용 기술 |
|---|---|
| Framework | React 19, TypeScript 6, Vite 8 |
| Routing | React Router |
| Server state | TanStack Query |
| Client state | Zustand |
| Styling | Tailwind CSS 4 |
| HTTP | Axios |
| Validation | Zod |
| Mock | MSW 2 |
| Date | date-fns |
| Quality | ESLint, Prettier |
| Deploy | Vercel |
정확한 버전은 package.json을 기준으로 합니다.
- 이메일·Google 로그인 및 회원가입
- 약관 동의와 프로필 온보딩
- 프로젝트 생성·관리, 일정, 공지, 파일, 영상 피드백(답글 포함)
- 팀원 초대와 영상 공유 링크를 통한 게스트 피드백
- 구인구직 공고와 공개 프로필, 파일 첨부 지원
- 마이페이지와 계정 설정
| 영역 | 화면 |
|---|---|
| 진입 | 랜딩, 로그인, 이메일 회원가입, 약관 동의, 비밀번호 재설정, 프로젝트 초대 수락 |
| 온보딩 | 역할, 지역, 카테고리, 프로필 설정 |
| 홈 | 오늘의 브리핑, 프로젝트, 추천 공고, 미니 캘린더, 알림 |
| 구인구직 | 공고 목록·작성·상세, 지원(파일 첨부), 지원자 확인 |
| 프로필 | 마이 프로필, 공개 프로필, 북마크 |
| 워크스페이스 | 프로젝트 목록·설정, 공지, 최근 활동 |
| 프로젝트 상세 | 대시보드, 일정, 파일, 영상, 피드백과 답글 |
| 계정 설정 | 알림, 비밀번호 변경, 문의, 회원 탈퇴 |
flowchart TD
A[랜딩] --> B[로그인]
B --> C{사용자 상태}
C -->|기존 사용자| D[홈]
C -->|신규 사용자| E[회원가입] --> F[약관 동의] --> G[온보딩] --> D
C -->|영상 공유 링크| H[게스트 영상 피드백]
C -->|프로젝트 초대 링크| M[초대 수락] --> D
D --> I[구인구직]
D --> J[프로필]
D --> K[워크스페이스] --> L[프로젝트 상세]
src/
├── api/ # API client, endpoint paths, 요청·응답 변환
├── assets/ # 이미지, 아이콘, 폰트
├── components/ # 여러 도메인에서 쓰는 공용 UI
├── constants/ # 상태 라벨, 카테고리 등 상수
├── domains/ # 도메인별 UI와 기능 묶음
├── hooks/ # 공용 커스텀 훅
├── layouts/ # 공용 레이아웃
├── mocks/ # MSW browser worker와 handlers
├── pages/ # 페이지 단위 화면
├── queries/ # TanStack Query key, query, mutation
├── routes/ # React Router route 조각과 bridge
├── schemas/ # Zod schema
├── stores/ # Zustand store
├── styles/ # 공용 스타일 조합
├── types/ # 도메인 타입
└── utils/ # 순수 유틸리티
앱은 BrowserRouter와 Routes를 사용합니다.
- 워크스페이스 경로는
src/routes/workspace에서 선언적으로 관리합니다. - 전체 화면 진입 경로는
src/routes/fullscreen에서 관리합니다. - 일부 기존 화면은
App.tsx의 레거시 분기와NavigateBridge를 통해 React Router와 함께 동작합니다. - 대부분의 페이지는
React.lazy+Suspense로 라우트 단위 code splitting이 적용되어 있습니다.
새 경로는 가능한 한 routes/에 추가하고, 기존 navigate() 헬퍼를 사용할 때에도 Router 상태와 URL 상태가 함께 갱신되는지 확인합니다.
CI 기준 Node.js 버전은 20입니다.
npm ci
npm run devnpm run lint
npm run format:check
npm run build자동 포맷은 npm run format을 사용합니다.
.env.example을 복사해 .env.local을 만들고 목적에 맞게 설정합니다.
| 목적 | VITE_ENABLE_MSW |
VITE_API_BASE_URL |
설명 |
|---|---|---|---|
| 로컬 mock | true |
비움 | MSW가 /api/v1 요청을 가로챕니다. |
| 로컬 BE proxy | false |
비움 | Vite dev server가 /api를 API 서버로 proxy합니다. |
| 원격 BE 직접 연결 | false |
API origin | 브라우저가 API origin으로 직접 요청하므로 BE CORS 설정이 필요합니다. |
Vercel Preview·Production의 값은 저장소 파일이 아니라 Vercel 프로젝트 환경 변수에서 관리합니다. 실제 BE를 연결할 배포에서는 MSW를 끄고, 해당 배포 환경에 맞는 API base URL을 설정해야 합니다.
- 실제 BE 모드에서 소셜 로그인은 인증 콜백 후 사용자 상태를 조회해 홈 또는 온보딩(신규 사용자는 약관 동의 포함)으로 이동합니다.
- 프로필 단계는
POST /api/v1/users/onboarding으로 닉네임, 역할, 지역, 카테고리, 소개, 약관 동의 값을 전송합니다. - 프로필 이미지는 별도 업로드 흐름을 통해 처리한 뒤, 서버가 사용할 수 있는 URL만 온보딩 요청에 포함합니다.
- 프로젝트 초대 수락도 비슷한 구조로, 역할 선택 후 약관에 동의하면 초대를 수락합니다.
- MSW 모드에서는 위 흐름을 mock handler가 대체합니다.
폰트와 색상 토큰은 src/index.css의 @theme에서 관리합니다. 화면에서는 semantic color token을 우선 사용하고, 도메인 전용 스타일 조합은 src/styles에 둡니다.
폰트: Pretendard
| 구분 | 기준 |
|---|---|
| 타이포그래피 | text-head-*, text-body-*, text-caption-*과 굵기 클래스(font-bold/font-semibold/font-normal)를 조합합니다. |
| 색상 | Semantic token(bg-primary, text-neutral-6 등)을 우선 사용하고, 필요할 때만 primitive token을 직접 사용합니다. |
| 공용 UI | 여러 도메인에서 쓰는 UI는 src/components, 도메인 전용 UI는 src/domains에 둡니다. |
Input · TextArea · Select · Choice · Button 등 폼 컨트롤을 여러 명이 동시에 만들 때 props가 어긋나 폼 화면에서 충돌하는 것을 막기 위한 최소 규약입니다. 기준 템플릿은
src/components/TextArea.tsx.
폼 컴포넌트는 다음 역할을 구분합니다.
| 컴포넌트 | 용도 |
|---|---|
Input |
문자열 입력 |
TextArea |
여러 줄 텍스트 입력 |
FileInput |
파일 선택·업로드 |
Choice |
checkbox·radio 선택 |
Select |
값 선택 드롭다운 |
Switch |
boolean ON/OFF |
ActionMenu |
값 입력이 아닌 액션 목록 — 트리거 + items[] + 열림/닫기로 구성. 공통 props 규약 미적용 |
공용 폼 컴포넌트는 controlled props를 사용합니다.
| prop | 타입 | 동작 |
|---|---|---|
label |
string? |
라벨. input id와 htmlFor로 연결 (id 미전달 시 useId) |
required |
boolean? |
true면 label 옆 * 표시 + input에 aria-required |
hint |
string? |
안내 문구. error가 없을 때만 회색 표시 |
error |
string? |
에러 메시지. 있으면 hint보다 우선, text-warning 색 |
value / onChange |
controlled | 값은 부모가 보유 |
disabled |
boolean? |
비활성 상태 |
// 라벨 — input의 id와 htmlFor 연결, *는 필수 표시
{
label && (
<label htmlFor={id}>
{label}
{required && <span className="text-warning">*</span>}
</label>
)
}
// 메시지 — error가 hint보다 우선
{
;(error || hint) && (
<span className={error ? 'text-warning' : 'text-neutral-5'}>{error ?? hint}</span>
)
}variant/size는 이름·값 종류만 통일하고 실제 Tailwind 매핑은 컴포넌트마다 자유입니다(Button의 primary ≠ Tag의 primary 생김새).
// src/types/ui.ts
export type Variant = 'primary' | 'secondary' | 'ghost'
export type Size = 'sm' | 'md' | 'lg'이미 작업 중인 컴포넌트는 갈아엎지 않고 다음 작업분부터 적용합니다. 반대 의견은 PR 또는 팀 채널에 회신하고, 없으면 이대로 진행합니다.
- 스키마와 제출 검증은 페이지 또는 상위 도메인이 소유합니다.
- 공용 컴포넌트는 검증 규칙 대신
error문자열을 받아 표시합니다. - 필드별 검증은 필요할 때
onBlur와validateField로 처리하고, 제출 시에는 전체 schema를 다시 검증합니다. - 교차 필드 규칙은 base schema와 refine schema를 분리해, 필드 검증과 전체 제출 검증이 모두 가능하도록 합니다.
.refine()이 붙은 스키마는.shape가 없으므로, 필드별 검증을 위해 base 객체와 refine을 분리해야 합니다.
// src/schemas/...
const base = z.object({ password: /* ... */, passwordConfirm: /* ... */ })
export const signupSchema = base.refine((v) => v.password === v.passwordConfirm, {
message: '비밀번호가 일치하지 않습니다',
path: ['passwordConfirm'],
})
// 필드별 blur 검증 → base.shape.password (.shape 살아있음)
// 제출 검증 → signupSchema.safeParse(전체 값)- API endpoint와 응답 정규화는
src/api에서 관리합니다. BE 응답 형태가 도메인 모델과 다를 때는normalize.ts,scheduleNormalize.ts같은 변환 계층에서 맞춥니다. - 서버 데이터는
src/queries의 query key, query, mutation으로 관리합니다. - 변경 mutation 뒤에는 해당 화면뿐 아니라 영향을 받는 목록·상세 query를 함께 무효화합니다.
서버에서 온 데이터는 위 TanStack Query가 관리합니다. 그 외 클라이언트 전용 상태는 아래 기준으로 위치를 정합니다.
| 상태 종류 | 도구 | 기준 | 예시 |
|---|---|---|---|
| 컴포넌트 로컬 상태 | useState |
하나의 컴포넌트(또는 직계 자식)에서만 쓰는 값. 기본 선택지 | 폼 입력값, 드롭다운·모달 열림 여부 |
| 레이아웃-페이지 간 구조적 통신 | Context API | 페이지가 자신을 감싸는 레이아웃(헤더 등)에 값을 실어 보내야 하는, 구조가 고정된 관계에서만 사용 | HeaderSlotContentContext/HeaderSlotActionsContext(src/layouts/headerSlotContext.ts) — 페이지가 useHeaderSlot 훅으로 헤더 좌/우 영역 콘텐츠를 채웁니다 |
| 앱 전역 상태 | Zustand | 여러 화면·라우트를 오가며 유지돼야 하고, prop으로 내려주기엔 소비하는 곳이 흩어져 있는 값 | useUserStore(로그인 사용자), useOnboardingStore(온보딩 4단계 값) |
- 새 store는
src/stores/에create<State>(...)형태로 추가합니다. 여러 컴포넌트가 이미 prop drilling으로 값을 전달하고 있다면 Zustand로 옮길 신호로 봅니다. - Context를 새로 만들 때는 값(자주 바뀜)과 setter(안 바뀜)를 별도 Context로 분리해, 값이 바뀔 때 setter만 구독하는 컴포넌트까지 리렌더되지 않게 합니다.
dev에서 작업 단위별 브랜치를 생성합니다.
feature/{작성자}-{기능}
fix/{작성자}-{수정}
refactor/{작성자}-{대상}
docs/{작성자}-{문서}
모든 변경은 먼저 GitHub 이슈로 기록합니다. 종류에 맞는 이슈 템플릿을 사용합니다.
| 종류 | 템플릿 구성 |
|---|---|
| 기능 개발 | 작업 내용 · 상세 작업(체크리스트) · 완료 조건(체크리스트) · 참고(피그마 링크 등) |
| 버그 리포트 | 재현 방법 · 예상/실제 동작 · 스크린샷 · 환경 |
라벨은 템플릿 선택 시 enhancement/bug가 자동으로 붙습니다. 리포지토리에 커스텀 feature 라벨은 없습니다.
커밋 제목에는 이슈 번호를 포함하고, 별도의 trailer는 사용하지 않습니다.
type: 작업 내용 (#이슈번호)
예시:
fix: 파일 업로드 제한을 수정 (#307)
| type | 설명 |
|---|---|
feat |
새로운 기능 |
fix |
버그 수정 |
style |
코드 포맷, 세미콜론 등 (로직 변경 없음) |
refactor |
리팩토링 |
chore |
빌드 설정, 패키지 관리 |
docs |
문서 수정 |
- 기능·화면·문서 등 검토 가능한 단위로 PR을 나눕니다.
- 일반 작업 PR의 base는
dev입니다. - 제목은 커밋 규칙과 같은
type: 작업 내용 (#이슈번호)형식으로 작성합니다. - 본문은 PR 템플릿에 맞춰
Closes #이슈번호, 변경 사항, 스크린샷, 리뷰 포인트, 체크리스트를 작성합니다. UI 변경이 있으면 스크린샷을 첨부하고, 문서-only PR은 «해당 없음»으로 표기합니다. - 팀장 리뷰 후 머지합니다.
dev병합 뒤 통합 테스트를 진행하고, 배포 시점에만dev를main으로 병합합니다.
| 대상 | 규칙 | 예시 |
|---|---|---|
| 컴포넌트 파일/폴더 | PascalCase | VideoPlayer.tsx |
| 함수 · 변수 · 커스텀 훅 | camelCase | isLoggedIn, useAuth.ts |
| 레포지토리 · 브랜치 | kebab-case | slate-to-fe, feature/kcleverp-login |
- 페이지는 라우팅·데이터 조립을 담당하고, 화면의 독립적인 상호작용은 도메인 컴포넌트 또는 훅으로 분리합니다.
- 사용하지 않는 상태·임시 데이터·디버깅 코드는 PR 전에 제거합니다.
- 비동기 요청의 로딩·오류·취소 또는 최신 요청 보장 방식을 명확히 처리합니다.