# 비트업 프론트엔드 개발 가이드

## Plugins

### Mobile Check

```javascript
const { $isMobile } = useNuxtApp();
// 브라우저 User Agent 기반 모바일 체크 (boolean)
```

## Composables

### useAuth

`composables/useAuth.js`는 인증 관련 기능을 제공하는 시나리오 테스트용 composable입니다.
(추후 개발 작업시 삭제)

#### 사용 예시

```javascript
const { login, logout, isLoggedIn, currentUser, checkAuth } = useAuth();

// 로그인
const result = login('test1234@test.com', 'test1234');
if (result.success) {
  // 로그인 성공
}

// 로그아웃
logout();

// 인증 상태 확인
const isAuth = checkAuth();
```

### useDevice

```javascript
import { useDevice, useResizeListener } from "@/composables/useDevice";

// 반응형 모바일 체크 (window.innerWidth <= 992)
// resize 이벤트에 자동으로 반응 (throttle 150ms)
const { isMobile } = useDevice();

// 전역 resize 이벤트에 커스텀 리스너 등록
useResizeListener(() => {
  // resize 시 실행할 로직
});
```

### useBettingStats

```javascript
import { useBettingStats } from "@/composables/useBettingStats";

const {
  bettingStats,        // 배팅 통계 상태 (ref)
  upWinAmount,        // UP 승리 금액 (computed)
  downWinAmount,      // DOWN 승리 금액 (computed)
  addMyBetting,       // 내 배팅 추가/업데이트 함수
  setWinner,          // 승자 설정 함수 ('up' | 'down' | null)
  setLoser,           // 패자 설정 함수 (반대편을 승자로)
  resetGameData,      // 게임 데이터 초기화 함수 (더미 데이터 포함)
  resetGameDataEmpty, // 게임 데이터 완전 초기화 함수
  gameResetCounter,   // 게임 리셋 카운터 (ref)
  animationStates,    // 애니메이션 상태 (ref)
} = useBettingStats();

// ===== bettingStats 객체 구조 =====
// bettingStats.value = {
//   // UP 관련 통계
//   upTotalBetting: 0,        // 내가 UP에 배팅한 총 금액
//   upExpectedProfit: 0,      // UP 예상 수익
//   upWin: 0,                // UP에 배팅한 모든 금액 (더미 + 내 금액)
//   upPeopleCount: 0,        // UP 배팅 참여자 수
//   upParticipants: [],      // UP 참여자 목록
//   upShowResult: false,     // 게임 종료 시 결과 영역 표시 여부
//   
//   // DOWN 관련 통계
//   downTotalBetting: 0,     // 내가 DOWN에 배팅한 총 금액
//   downExpectedProfit: 0,   // DOWN 예상 수익
//   downLose: 0,             // DOWN에 배팅한 모든 금액 (더미 + 내 금액)
//   downPeopleCount: 0,      // DOWN 배팅 참여자 수
//   downParticipants: [],    // DOWN 참여자 목록
//   downShowResult: false,   // 게임 종료 시 결과 영역 표시 여부
//   
//   // 결과 정보
//   winner: 'up' | 'down' | null,  // 승자 ('up' | 'down' | null)
// }

// ===== 참여자(participant) 객체 구조 =====
// participant = {
//   id: string,        // 고유 ID
//   name: string,      // 참여자 이름
//   amount: number,    // 배팅 금액
//   isMy: boolean,     // 내 배팅 여부
// }

// ===== animationStates 객체 구조 =====
// animationStates.value = {
//   upAnimationComplete: false,        // UP 애니메이션 완료 여부
//   downAnimationComplete: false,      // DOWN 애니메이션 완료 여부
//   upCountAnimationComplete: false,   // UP 카운트 애니메이션 완료 여부
//   downCountAnimationComplete: false, // DOWN 카운트 애니메이션 완료 여부
//   upResultExpanded: false,           // UP 결과 영역 확장 여부
//   downResultExpanded: false,         // DOWN 결과 영역 확장 여부
//   upWidthAnimationComplete: false,   // UP width 애니메이션 완료 여부
//   downWidthAnimationComplete: false, // DOWN width 애니메이션 완료 여부
//   bothCountAnimationsComplete: false, // 두 카운트 애니메이션 모두 완료 여부
// }

// ===== 사용 예시 =====

// 내 배팅 추가/업데이트
addMyBetting('up', 100000);  // UP에 100,000원 배팅
addMyBetting('down', 50000); // DOWN에 50,000원 배팅

// 승자 설정
setWinner('up');    // UP을 승자로 설정
setWinner('down');  // DOWN을 승자로 설정
setWinner(null);    // 승자 없음

// 패자 설정 (반대편을 승자로)
setLoser('up');     // UP을 패자로 설정 (DOWN이 승자)
setLoser('down');   // DOWN을 패자로 설정 (UP이 승자)

// 게임 데이터 초기화 (더미 데이터 포함)
resetGameData();  // 새 게임 시작 시 사용

// 게임 데이터 완전 초기화 (모든 데이터 제거)
resetGameDataEmpty();  // 빈 상태로 시작

// 게임 리셋 카운터 증가 (motion 컴포넌트 강제 리렌더링)
gameResetCounter.value += 1;

// 배팅 통계 접근
bettingStats.value.upTotalBetting = 100000;
bettingStats.value.downTotalBetting = 50000;
bettingStats.value.winner = 'up';
bettingStats.value.upShowResult = true;  // 결과 표시

// 애니메이션 상태 접근
animationStates.value.upAnimationComplete = true;
animationStates.value.bothCountAnimationsComplete = true;

// 승리 금액 계산 (computed)
const upWin = upWinAmount.value;      // UP 승리 금액
const downWin = downWinAmount.value;  // DOWN 승리 금액
```

### useForm

```javascript
import {
  useFormField,
  usePasswordField,
  usePasswordConfirmField,
  useFormValidation,
  validateEmail,
  validatePassword,
} from "@/composables/useForm";

// 기본 필드
const email = useFormField("", {
  required: true,
  validator: validateEmail,
  errorMessages: {
    empty: "이메일을 입력해주세요.",
    invalid: "이메일 형식이 올바르지 않습니다.",
  },
});

// 비밀번호 필드
const password = usePasswordField("", {
  required: true,
  errorMessages: {
    empty: "비밀번호를 입력해주세요.",
    invalid: "비밀번호는 영문숫자 포함 8자 이상이어야 합니다.",
  },
});

// 비밀번호 확인 필드
const passwordConfirm = usePasswordConfirmField("", password, {
  errorMessages: {
    empty: "비밀번호를 입력해주세요.",
    mismatch: "비밀번호가 일치하지 않습니다.",
  },
});

// 폼 검증
const form = useFormValidation(
  { email, password },
  {
    onSubmit: async (values) => {
      // 폼 제출 로직
      console.log(values);
      return true; // 성공 시 true 반환
    },
  }
);

// 사용
await form.handleSubmit();
```

### useChat

```javascript
import { useChat } from "@/composables/useSidebar";

const {
  isChatOpen,              // 채팅 열림 상태
  unreadMessageCount,      // 읽지 않은 메시지 수
  toggleChat,              // 채팅 토글
  openChat,                // 채팅 열기
  closeChat,               // 채팅 닫기
  isNotificationOpen,      // 알림 열림 상태
  toggleNotification,      // 알림 토글
  openNotification,        // 알림 열기
  closeNotification,       // 알림 닫기
  unreadNotificationCount, // 읽지 않은 알림 수
} = useChat();
```

### useContainerQuery

```javascript
import { useContainerQuery } from "@/composables/useContainerQuery";

const containerRef = ref(null);

// 컨테이너 크기 기반 반응형 체크
// breakpoint: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
// throttleDelay: 기본 16ms (한 프레임)
const { isBelowBreakpoint } = useContainerQuery(
  containerRef,
  "xxl",  // 1400px
  16      // throttle delay (ms)
);

// 사용 예시
// <div ref="containerRef">...</div>
// v-if="!isBelowBreakpoint" // 1400px 초과일 때만 표시
```

### useDateFilter

```javascript
import { useDateFilter } from "@/composables/useDateFilter";

const {
  dateOptions,        // 날짜 선택 옵션 (지난 90일, 60일, 30일, 7일, 24시간, 날짜 범위)
  selectedType,       // 선택된 타입
  selectRef,         // Select 컴포넌트 ref
  dateRange,         // 날짜 범위 { start, end }
  isDateInRange,     // 날짜 범위 체크 함수
  getDateRangeFromDays, // 지난 N일 날짜 범위 계산
  handleSelectChange,  // Select change 핸들러
  applyDateFilter,      // 날짜 필터링 적용 함수
} = useDateFilter();

// 사용 예시
const filteredTransactions = computed(() => {
  return applyDateFilter(transactions);
});
```

### useEmoji

```javascript
import { useEmoji } from "@/composables/useEmoji";

const { emojiCategories } = useEmoji();

// 이모지 카테고리 목록
// - smileys: 스마일리
// - gestures: 제스처
// - people: 사람
// - animals: 동물
// - food: 음식
// - activities: 활동
// - travel: 여행
// - objects: 물건
// - symbols: 기호
// - flags: 국기

// 각 카테고리는 { id, name, icon, emojis[] } 형태
```

### Storage

```javascript
import storage from "@/composables/common/storage";

// LocalStorage (JSON 자동 직렬화/역직렬화)
storage.setStorage("key", value);
const value = storage.getStorage("key");
storage.removeStorage("key");

// SessionStorage (JSON 자동 직렬화/역직렬화)
storage.setSession("key", value);
const value = storage.getSession("key");
storage.removeSession("key");
```

**직접 localStorage 사용 (인트로 페이지 예시):**

```javascript
// 저장
if (process.client) {
  localStorage.setItem('entered_intro', JSON.stringify({
    timestamp: Date.now(),
    entered: true,
  }));
}

// 읽기
if (process.client) {
  const data = localStorage.getItem('entered_intro');
  if (data) {
    const parsed = JSON.parse(data);
    // 사용
  }
}

// 삭제
if (process.client) {
  localStorage.removeItem('entered_intro');
}
```

### OAuth

```javascript
import oauth from "@/composables/common/oauth";

// PKCE 생성 (state, codeChallenge 반환)
// code_verifier는 자동으로 sessionStorage에 저장됨
const { state, codeChallenge } = await oauth.createPkce();

// Code Verifier 가져오기 (사용 후 자동 삭제)
const verifier = oauth.consumeCodeVerifier(state);

// 토큰 발급
const tokenData = await oauth.token(code, state, verifier);
// { access_token, refresh_token, ... }

// 토큰 갱신
const newTokenData = await oauth.refreshToken(refresh_token);
```

### Axios

```javascript
import axios from "@/composables/common/axios";

// 기본 axios 인스턴스 사용
// 자동으로 허용된 도메인 체크 및 401 에러 처리
const response = await axios({
  url: "/api/endpoint",
  method: "post",
  headers: {
    "Content-Type": "application/json",
  },
  data: { /* ... */ },
});
```

## Middleware

### Intro Middleware

인트로 페이지 접근 제어를 위한 전역 미들웨어입니다.

**파일 위치:**
- `middleware/intro.global.js`

**동작 방식:**
- 클라이언트 사이드에서만 실행
- 인트로 페이지(`/intro`)에서는 체크하지 않음
- localStorage에 `entered_intro` 값이 있고 6시간 이내면 통과
- 6시간이 지났거나 값이 없으면 `/intro`로 리다이렉트

**localStorage 데이터 구조:**

```javascript
{
  timestamp: 1234567890123,  // Date.now() 값
  entered: true
}
```

**사용 예시:**

```javascript
// middleware/intro.global.js
export default defineNuxtRouteMiddleware((to) => {
  // 클라이언트 사이드에서만 실행
  if (process.server) return;

  // 인트로 페이지에서는 체크하지 않음
  if (to.path === '/intro') return;

  try {
    const enteredData = localStorage.getItem('entered_intro');
    
    if (enteredData) {
      const data = JSON.parse(enteredData);
      const timestamp = data.timestamp;
      const now = Date.now();
      const sixHours = 6 * 60 * 60 * 1000;

      // 6시간이 지났으면 삭제하고 인트로로 리다이렉트
      if (now - timestamp > sixHours) {
        localStorage.removeItem('entered_intro');
        return navigateTo('/intro');
      }
      // 유효하면 통과
      return;
    }
  } catch (e) {
    console.error('LocalStorage error:', e);
    if (process.client) {
      localStorage.removeItem('entered_intro');
    }
  }

  // 인트로를 거치지 않았으면 인트로로 리다이렉트
  return navigateTo('/intro');
});
```

## Pages

### Page Meta

```javascript
definePageMeta({
  layout: "main",  // 'main' | 'mypage'
});
```

### Page Head

```javascript
useHead({
  title: "UpBit | Page Title",
});
```

### Intro Page

인트로 페이지는 첫 방문 시 필수로 거쳐야 하는 페이지입니다.

**특징:**
- `/` (루트) 접속 시 자동으로 `/intro`로 리다이렉트
- START 버튼 클릭 시 메인 페이지로 이동
- localStorage에 저장되어 6시간 동안 인트로 스킵 가능
- motion-v를 사용한 입장 애니메이션

**파일 위치:**
- `pages/intro/index.vue`

**사용 예시:**

```vue
<script setup>
import { motion } from "motion-v";

useHead({
  title: "UpBit",
});

// START 버튼 클릭 핸들러
const handleStart = () => {
  // localStorage에 저장
  if (process.client) {
    localStorage.setItem('entered_intro', JSON.stringify({
      timestamp: Date.now(),
      entered: true,
    }));
    
    // 메인 페이지로 이동
    navigateTo('/');
  }
};
</script>
```

**미들웨어:**
- `middleware/intro.global.js` - 전역 미들웨어로 모든 라우트에 적용
- 인트로를 거치지 않았거나 6시간이 지난 경우 자동으로 `/intro`로 리다이렉트

## Components

### Common Components

#### RollingCounter

```vue
<CommonRollingCounter
  :value="12345"
  suffix="K"
  typography="p1"
/>
```

#### Notification

```vue
<CommonNotification />
```

#### ChatSection

```vue
<CommonChatSection />
```

### Layout Components

```vue
<LayoutHeader />
<LayoutFooter />
<LayoutMyPageNav />
```

#### LayoutHeader와 인증 모달

로그인, 회원가입, 비밀번호 찾기는 별도 페이지 없이 **헤더 모달**로만 제공됩니다.

- **로그인 모달**: 헤더 "로그인" 버튼 클릭 시 열림
- **회원가입 모달**: 헤더 "가입" 버튼 또는 로그인 모달 내 "계정 만들기" 클릭 시 열림
- **비밀번호 찾기 모달**: 로그인 모달 내 "비밀번호를 잊어버리셨나요?" 클릭 시 열림  
  - 로그인 모달이 닫히고 비밀번호 찾기 모달이 열림  
  - 비밀번호 찾기 모달을 닫으면 로그인 모달이 다시 열림 (잭팟 모달과 동일한 `previous-modal` 패턴)

#### Modal

모달 컴포넌트는 `previous-modal` prop으로 **모달→모달** 흐름을 지원합니다. 자식 모달을 닫을 때 이전 모달을 다시 열 수 있습니다.

```vue
<Modal ref="childModal" title="자식 모달" :previous-modal="reopenParentModal" @close="handleClose">
  <!-- ... -->
</Modal>
```

**사용 패턴:**

1. 부모 모달을 열어 둔 상태에서 자식 모달을 열 때: 부모를 먼저 닫고 자식 모달을 연다.
2. 자식 모달에 `:previous-modal="reopenParentModal"` 을 전달한다. (`reopenParentModal`은 부모 모달의 `open()`을 호출하는 함수)
3. 자식 모달이 닫힐 때(닫기 버튼, 배경 클릭, ESC) `previous-modal`이 호출되어 부모 모달이 다시 열린다.

**Modal ref 메서드:**

- `open()`: 모달 열기
- `close()`: 모달 닫기 (previous-modal이 있으면 먼저 호출 후 닫기)
- `isOpened()`: 모달 열림 여부 반환

### Main Components

```vue
<MainBettingStatus />
<MainChartSection />
<MainBettingHistory />
<MainRankingHistory />
```

## Utils

### Throttle

```javascript
import { throttle } from "@/composables/utils/throttle";

// 함수 스로틀링 (지정된 시간 간격으로 실행 제한)
// 마지막 호출 후 delay 시간이 지나면 실행
const throttledFunction = throttle(() => {
  // 실행할 함수
}, 300); // 300ms 간격

// 사용 예시
window.addEventListener("resize", throttle(() => {
  // resize 처리
}, 150));
```

## API Keys

### Giphy API Key

```javascript
// CommonChatSection.vue에서 사용
// 개발용 API Key (배포 시 변경 필요)
const apiKey = "qxiEb2d61xFQrwj7IE5uw6E32RY1tMDN";
```

## 스타일 가이드

### SCSS 구조

```
assets/scss/
├── base/          # 기본 스타일
│   ├── _reset.scss        # CSS 리셋
│   ├── _variables.scss    # 변수 정의
│   ├── _mixins.scss       # Mixins
│   ├── _typography.scss   # 타이포그래피
│   ├── _icons.scss        # 아이콘 스타일
│   ├── _theme.scss        # 테마 설정
│   └── _function.scss     # 함수
├── components/    # 컴포넌트별 스타일
├── layout/        # 레이아웃 스타일
└── pages/         # 페이지별 스타일
```

### 반응형 브레이크포인트

- 모바일: `<= 992px`
- 데스크탑: `> 992px`

### 테마 시스템

#### 테마 설정 (`_theme.scss`)

프로젝트는 Dark/Light 테마를 지원합니다. 테마는 `html` 요소의 `data-theme` 속성으로 제어됩니다.

```scss
// 기본 테마 (Dark)
html,
html[data-theme="dark"] {
  @include theme-map($theme-dark);
}

// Light 테마
html[data-theme="light"] {
  @include theme-map($theme-light);
}
```

#### 컬러 변수 정의 (`_variables.scss`)

컬러 변수는 `_variables.scss`에서 정의되며, 테마별로 다른 값을 가집니다.

```scss
// CSS 변수명 선언
$container: --container;
$primary: --primary;
$text-high: --text-high;
// ... 등

// Dark 테마 컬러셋
$theme-dark: (
  $container: to-rgba(#ffffff, 0.09),
  $primary: to-rgb(#40fab6),
  $text-high: to-rgb(#ffffff),
  // ... 등
);

// Light 테마 컬러셋
$theme-light: (
  $container: to-rgba(#ffffff, 0.56),
  $primary: to-rgb(#00c980),
  $text-high: to-rgb(#111111),
  // ... 등
);
```

#### 컬러 사용 방법

**1. 정의된 컬러 변수 사용 (권장)**

```scss
.my-component {
  background-color: var(--container);
  color: var(--text-high);
  border: 1px solid var(--border-low);
}
```

**2. 스타일 가이드에 없는 컬러 사용**

컬러 변수에 정의되지 않은 컬러를 사용해야 할 경우, 컴포넌트 내부에서 직접 테마별로 선언합니다.

```scss
.my-component {
  // 공통 스타일
  padding: 1rem;
  
  // Dark 테마 전용 컬러
  [data-theme="dark"] & {
    background-color: rgba(255, 255, 255, 0.1);
    color: #ffffff;
  }
  
  // Light 테마 전용 컬러
  [data-theme="light"] & {
    background-color: rgba(0, 0, 0, 0.05);
    color: #111111;
  }
}
```

**3. 컬러 변수 목록**

사용 가능한 주요 컬러 변수:

- **Surface**: `--container`, `--invert-container`, `--none`
- **Primary**: `--primary`
- **Secondary**: `--secondary`
- **Warning**: `--warning`
- **Text**: `--text-high`, `--text-mid`, `--text-low`, `--text-yellow`
- **Border**: `--border-low`, `--border-mid`, `--border-high`
- **Modal**: `--modal-bg`, `--modal-surface`, `--modal-note`
- **Button**: `--btn-bg`, `--btn-line`, `--disabled`
- **Table**: `--table-th-bg`, `--table-tb-bg`, `--table-th-border`, `--table-tb-border`

**4. 컬러 변환 함수**

`_function.scss`에서 제공하는 컬러 변환 함수:

```scss
// Hex to RGB 변환
to-rgb(#40fab6)  // → "64, 250, 182"

// Hex to RGBA 변환
to-rgba(#ffffff, 0.09)  // → "rgba(255, 255, 255, 0.09)"
```

### 아이콘 사용

```vue
<!-- 아이콘 클래스 사용 -->
<span class="ico-before ico-user-primary">사용자</span>
<span class="ico-after ico-chevron-right">더보기</span>

<!-- 아이콘 파일 경로 -->
assets/icons/dark/  # 다크 모드 아이콘
assets/icons/light/  # 라이트 모드 아이콘
```

### 스타일 충돌

- `scoped` 스타일 사용
- 전역 스타일은 `assets/scss/style.scss`에서 관리


