Jelajahi Sumber

+ 바이브코딩 작업분

송용우 4 bulan lalu
induk
melakukan
5ec79e797d

+ 8 - 0
.cursor/rules/api-rule.mdc

@@ -0,0 +1,8 @@
+---
+alwaysApply: true
+---
+
+
+- api 서버는 코드이그나이터4 베이스의 벡엔드 기술로 구현되어있으며
+  기존 문서에사용되는 양식을 지키며 구현
+- 프론트에서 api신규 생성시 백엔드 코드이그나4 기반의 기술로 구현하는 예제를 함께 제공

+ 8 - 0
.cursor/rules/cursor-rule.mdc

@@ -0,0 +1,8 @@
+---
+alwaysApply: true
+---
+
+
+- 항상 2setp의 문서 여백을 제공하여 코드를 보기좋게 정렬
+- css는 항상 scss, sass 문법을 유지하며 중복되는 css는 정리하여 하나로 정의
+- 코드 작성시 반응형 대응을 고려하여 생성

+ 63 - 0
.cursor/rules/vite-rule.mdc

@@ -0,0 +1,63 @@
+---
+alwaysApply: true
+---
+  You are an expert in Laravel, Vue.js, and modern full-stack web development technologies.
+
+  Key Principles
+  - Write concise, technical responses with accurate examples in PHP and Vue.js.
+  - Follow Laravel and Vue.js best practices and conventions.
+  - Use object-oriented programming with a focus on SOLID principles.
+  - Favor iteration and modularization over duplication.
+  - Use descriptive and meaningful names for variables, methods, and files.
+  - Adhere to Laravel's directory structure conventions (e.g., app/Http/Controllers).
+  - Prioritize dependency injection and service containers.
+
+  Laravel
+  - Leverage PHP 8.2+ features (e.g., readonly properties, match expressions).
+  - Apply strict typing: declare(strict_types=1).
+  - Follow PSR-12 coding standards for PHP.
+  - Use Laravel's built-in features and helpers (e.g., `Str::` and `Arr::`).
+  - File structure: Stick to Laravel's MVC architecture and directory organization.
+  - Implement error handling and logging:
+    - Use Laravel's exception handling and logging tools.
+    - Create custom exceptions when necessary.
+    - Apply try-catch blocks for predictable errors.
+  - Use Laravel's request validation and middleware effectively.
+  - Implement Eloquent ORM for database modeling and queries.
+  - Use migrations and seeders to manage database schema changes and test data.
+
+  Vue.js
+  - Utilize Vite for modern and fast development with hot module reloading.
+  - Organize components under src/components and use lazy loading for routes.
+  - Apply Vue Router for SPA navigation and dynamic routing.
+  - Implement Pinia for state management in a modular way.
+  - Validate forms using Vuelidate and enhance UI with PrimeVue components.
+  
+  Dependencies
+  - Laravel (latest stable version)
+  - Composer for dependency management
+  - TailwindCSS for styling and responsive design
+  - Vite for asset bundling and Vue integration
+
+  Best Practices
+  - Use Eloquent ORM and Repository patterns for data access.
+  - Secure APIs with Laravel Passport and ensure proper CSRF protection.
+  - Leverage Laravel’s caching mechanisms for optimal performance.
+  - Use Laravel’s testing tools (PHPUnit, Dusk) for unit and feature testing.
+  - Apply API versioning for maintaining backward compatibility.
+  - Ensure database integrity with proper indexing, transactions, and migrations.
+  - Use Laravel's localization features for multi-language support.
+  - Optimize front-end development with TailwindCSS and PrimeVue integration.
+
+  Key Conventions
+  1. Follow Laravel's MVC architecture.
+  2. Use routing for clean URL and endpoint definitions.
+  3. Implement request validation with Form Requests.
+  4. Build reusable Vue components and modular state management.
+  5. Use Laravel's Blade engine or API resources for efficient views.
+  6. Manage database relationships using Eloquent's features.
+  7. Ensure code decoupling with Laravel's events and listeners.
+  8. Implement job queues and background tasks for better scalability.
+  9. Use Laravel's built-in scheduling for recurring processes.
+  10. Employ Laravel Mix or Vite for asset optimization and bundling.
+  

+ 670 - 0
.cursor/rules/vooster__architecture.mdc

@@ -0,0 +1,670 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+# Technical Requirements Document (TRD)
+
+## 1. Executive Technical Summary
+- **프로젝트 개요**  
+  인플루언서와 벤더사 간 수·발주, 배송, 정산, 알림을 웹 기반으로 자동화하는 통합 플랫폼. 오프라인 문서 교환 제거, 실시간 상태 관리, 파트너 매칭 기능 제공.
+- **핵심 기술 스택**  
+  Frontend: Vue 3 + Nuxt3, Vuetify, TypeScript  
+  Backend: CodeIgniter4 REST API + Node.js BFF  
+  DB: MySQL(RDS), Redis 캐시  
+  배포: Docker, Kubernetes, GitHub Actions CI/CD  
+  OCR: Google Cloud Vision API  
+  모니터링: ELK 스택, Grafana  
+- **주요 기술 목표**  
+  평균 응답시간 300ms 이하, 동시 5,000 사용자 처리, 가용성 99.9%, 서버 오류율 <1%
+- **주요 가정**  
+  - 초기 규모: 월 거래액 10억, DAU 1,000+  
+  - 클라우드 인프라(AWS/GCP) 사용  
+  - 외부 택배사·ERP API 연동 가능성 상시 고려  
+
+## 2. Tech Stack
+
+| Category           | Technology / Library           | Reasoning (선택 이유)                                              |
+| ------------------ | ------------------------------ | ----------------------------------------------------------------- |
+| Frontend Framework | Vue 3 + Nuxt3                  | SSR/SSG 지원으로 초기 로딩 최적화, SEO 강화                       |
+| UI Library         | Vuetify                        | 머티리얼 디자인 기반, 빠른 UI 컴포넌트 구성                       |
+| Language           | TypeScript                     | 정적 타입 검사로 코드 안정성 및 가독성 확보                       |
+| State Management   | Pinia                          | Composition API 친화적, 러닝 커브 완만                           |
+| HTTP Client        | Axios                          | Promise 기반, 요청/응답 인터셉터 활용 용이                        |
+| Backend Framework  | CodeIgniter 4                  | 경량 PHP 프레임워크, 빠른 개발 및 유지보수                        |
+| API Layer (BFF)    | Node.js + Express              | 프론트엔드 맞춤형 API 어댑터, 비즈니스 로직 경량 분리             |
+| Database           | MySQL (RDS)                    | 관계형 데이터 안정성·확장성, RDS 관리 편의성                     |
+| Cache              | Redis                          | 세션 관리, 빈번한 조회 데이터 캐싱으로 응답 속도 개선             |
+| Containerization   | Docker                         | 환경 일관성 확보, 배포 자동화                                     |
+| Orchestration      | Kubernetes                     | 자동 스케일링, 자가 복구, 클러스터 관리                           |
+| CI/CD              | GitHub Actions                 | 코드 푸시 시 빌드·테스트·배포 자동화                              |
+| OCR                | Google Cloud Vision API        | 높은 정확도, 이미지→텍스트 자동 변환                              |
+| Monitoring         | ELK (Elasticsearch, Logstash, Kibana), Grafana | 로그 집계·시각화, 메트릭 모니터링                                 |
+| Authentication     | JWT, OAuth2 (Google/Kakao/Naver) | 보안성 높은 인증, SNS 간편 로그인 지원                            |
+| Real-time          | WebSocket (Socket.IO)          | 실시간 알림(주문 상태, 승인 결과)                                |
+| Integration        | Courier API, ERP REST API      | 택배사 송장 조회, 회계 시스템 자동 연동                           |
+
+## 3. System Architecture Design
+
+### Top-Level building blocks
+- Frontend (Nuxt3): SSR 페이지, 컴포넌트, 인증/알림 UI  
+- BFF (Node.js + Express): Frontend 전용 경량 API 어댑터, 실시간 채널 관리  
+- Backend API (CI4): 핵심 비즈니스 로직, DB CRUD, 권한 관리  
+- Database & Cache: MySQL RDS, Redis 캐시 서버  
+- External Integrations: Google Vision OCR, 택배사 API, ERP API  
+- Monitoring & Logging: ELK 스택, Grafana 알림
+
+### Top-Level Component Interaction Diagram
+```mermaid
+graph TD
+    F[Nuxt3 Frontend] --> BFF(BFF: Node.js)
+    BFF --> API[Backend API: CI4]
+    API --> DB[MySQL(RDS)]
+    API --> Cache[Redis]
+    API --> OCR[Google Vision API]
+    API --> Courier[택배사 API]
+    API --> ERP[ERP API]
+    Monitoring --> API
+    Monitoring --> BFF
+```
+- Nuxt3 프론트엔드가 BFF로 요청 전달  
+- BFF는 세션/인증 관리 후 CI4 API 호출  
+- CI4 API는 MySQL/Redis, 외부 OCR·택배·ERP 연동  
+- ELK·Grafana로 전체 서비스 상태 모니터링  
+
+### Code Organization & Convention
+**Domain-Driven Organization Strategy**  
+- **도메인 분리**: 사용자, 주문, 배송, 정산, 매칭 등 비즈니스 도메인별 모듈  
+- **레이어 아키텍처**: Presentation, Application, Domain, Infrastructure  
+- **기능 기반 모듈화**: 각 도메인 기능을 독립 패키지로 관리  
+- **공유 컴포넌트**: Utils, Types, 공통 미들웨어, 인터셉터
+
+**Universal File & Folder Structure**
+```
+/
+├── app.vue
+├── assets
+│   ├── font
+│   │   ├── Inter-Medium.woff
+│   │   ├── NotoSansKR-Black.otf
+│   │   ├── NotoSansKR-Black.woff
+│   │   ├── NotoSansKR-Black.woff2
+│   │   ├── NotoSansKR-Bold.otf
+│   │   ├── NotoSansKR-Bold.woff
+│   │   ├── NotoSansKR-Bold.woff2
+│   │   ├── NotoSansKR-DemiLight.otf
+│   │   ├── NotoSansKR-DemiLight.woff
+│   │   ├── NotoSansKR-DemiLight.woff2
+│   │   ├── NotoSansKR-Light.otf
+│   │   ├── NotoSansKR-Light.woff
+│   │   ├── NotoSansKR-Light.woff2
+│   │   ├── NotoSansKR-Medium.otf
+│   │   ├── NotoSansKR-Medium.woff
+│   │   ├── NotoSansKR-Medium.woff2
+│   │   ├── NotoSansKR-Regular.otf
+│   │   ├── NotoSansKR-Regular.woff
+│   │   ├── NotoSansKR-Regular.woff2
+│   │   ├── NotoSansKR-Regular(1).woff
+│   │   ├── NotoSansKR-Thin.otf
+│   │   ├── NotoSansKR-Thin.woff
+│   │   └── NotoSansKR-Thin.woff2
+│   ├── img
+│   │   ├── bg_login.svg
+│   │   ├── bg_otp_reg.png
+│   │   ├── bg_popup.svg
+│   │   ├── bg_tab_off.svg
+│   │   ├── bg_tab_on.svg
+│   │   ├── bg_tooltip.svg
+│   │   ├── bg_tooltip2.svg
+│   │   ├── bg_tooltip3.svg
+│   │   ├── bg_tooltip4.svg
+│   │   ├── btn_app_store.svg
+│   │   ├── btn_goolge_play.svg
+│   │   ├── btn.png
+│   │   ├── caution_bg.jpg
+│   │   ├── db_set_list01.svg
+│   │   ├── db_set_list02.svg
+│   │   ├── db_set_list03.svg
+│   │   ├── head_flip_btn.svg
+│   │   ├── ic_add.svg
+│   │   ├── ic_allview.svg
+│   │   ├── ic_arrow_right_chv.svg
+│   │   ├── ic_avg01.svg
+│   │   ├── ic_avg02.svg
+│   │   ├── ic_avg03.svg
+│   │   ├── ic_avg04.svg
+│   │   ├── ic_card_nodata.svg
+│   │   ├── ic_card_off.svg
+│   │   ├── ic_card_on.svg
+│   │   ├── ic_chv_arrow.svg
+│   │   ├── ic_chv.svg
+│   │   ├── ic_close.svg
+│   │   ├── ic_drop_down_on.svg
+│   │   ├── ic_drop_down.svg
+│   │   ├── ic_ds.svg
+│   │   ├── ic_end_close_cl.svg
+│   │   ├── ic_end_close_x.svg
+│   │   ├── ic_end_close.png
+│   │   ├── ic_end_close.svg
+│   │   ├── ic_end_red.svg
+│   │   ├── ic_equip01.svg
+│   │   ├── ic_equip02.svg
+│   │   ├── ic_equip03.svg
+│   │   ├── ic_equip04.svg
+│   │   ├── ic_excel_green.svg
+│   │   ├── ic_excel.svg
+│   │   ├── ic_gear.svg
+│   │   ├── ic_google.svg
+│   │   ├── ic_grid_box.png
+│   │   ├── ic_home_arrow.svg
+│   │   ├── ic_info.svg
+│   │   ├── ic_issue_flag.svg
+│   │   ├── ic_kakao.svg
+│   │   ├── ic_list_off.svg
+│   │   ├── ic_list_on.svg
+│   │   ├── ic_map_card.svg
+│   │   ├── ic_map_pin.svg
+│   │   ├── ic_mapt_chv.svg
+│   │   ├── ic_more_btn.svg
+│   │   ├── ic_more_plust_gray.svg
+│   │   ├── ic_naver.svg
+│   │   ├── ic_no_img.svg
+│   │   ├── ic_no_tree.svg
+│   │   ├── ic_preview_nw.svg
+│   │   ├── ic_radio_off.svg
+│   │   ├── ic_radio_on.svg
+│   │   ├── ic_sch_nw.svg
+│   │   ├── ic_sts.svg
+│   │   ├── ic_tab01.svg
+│   │   ├── ic_tab02.svg
+│   │   ├── ic_tab03.svg
+│   │   ├── ic_tab04.svg
+│   │   ├── ic_tack_off.svg
+│   │   ├── ic_tack_on.svg
+│   │   ├── ic_tenant_small_white.svg
+│   │   ├── ic_tenant_small.svg
+│   │   ├── ic_tenant01.svg
+│   │   ├── ic_tenant02.svg
+│   │   ├── ic_tenant03.svg
+│   │   ├── ic_tenant04.svg
+│   │   ├── ic_wifi_dis.svg
+│   │   ├── ic_wifi.svg
+│   │   ├── ic_x_btn.svg
+│   │   ├── ic_x_btn2.svg
+│   │   ├── ic_xcircle.svg
+│   │   ├── ico_alarm_blue.svg
+│   │   ├── ico_alarm_gray.svg
+│   │   ├── ico_alarm_green.svg
+│   │   ├── ico_alarm_red.svg
+│   │   ├── ico_alarm1.svg
+│   │   ├── ico_alarm2.svg
+│   │   ├── ico_alarm3.svg
+│   │   ├── ico_alarm4.svg
+│   │   ├── ico_all_pop.svg
+│   │   ├── ico_arrow_next.svg
+│   │   ├── ico_arrow_prev.svg
+│   │   ├── ico_backup1.svg
+│   │   ├── ico_backup2.svg
+│   │   ├── ico_backup3.svg
+│   │   ├── ico_backup4.svg
+│   │   ├── ico_ban.svg
+│   │   ├── ico_bar.svg
+│   │   ├── ico_black_pin.svg
+│   │   ├── ico_blue_pin.svg
+│   │   ├── ico_btn1.svg
+│   │   ├── ico_btn2.svg
+│   │   ├── ico_btn3.svg
+│   │   ├── ico_cal_dis.svg
+│   │   ├── ico_cal.svg
+│   │   ├── ico_calendar.svg
+│   │   ├── ico_cancel_disabled.svg
+│   │   ├── ico_cancel.svg
+│   │   ├── ico_cate.svg
+│   │   ├── ico_certify_n.svg
+│   │   ├── ico_certify_y.svg
+│   │   ├── ico_certify_y2.svg
+│   │   ├── ico_certify_y3.svg
+│   │   ├── ico_check_indeterminate.svg
+│   │   ├── ico_chk_circle_disabled.svg
+│   │   ├── ico_chk_circle.svg
+│   │   ├── ico_chk_off.svg
+│   │   ├── ico_chk_off2.svg
+│   │   ├── ico_chk_on.svg
+│   │   ├── ico_chk.svg
+│   │   ├── ico_close_gray.svg
+│   │   ├── ico_close.svg
+│   │   ├── ico_core_alarm1.svg
+│   │   ├── ico_core_alarm2.svg
+│   │   ├── ico_date_pic.svg
+│   │   ├── ico_del_disabled.svg
+│   │   ├── ico_del_disabled2.svg
+│   │   ├── ico_del.svg
+│   │   ├── ico_del2.svg
+│   │   ├── ico_download.svg
+│   │   ├── ico_end.svg
+│   │   ├── ico_equip.svg
+│   │   ├── ico_eraser.svg
+│   │   ├── ico_eraser2.svg
+│   │   ├── ico_error.svg
+│   │   ├── ico_event_pop.svg
+│   │   ├── ico_event_view_black.png
+│   │   ├── ico_event_view_black.svg
+│   │   ├── ico_event_view_down.svg
+│   │   ├── ico_event_view.svg
+│   │   ├── ico_excel_d.svg
+│   │   ├── ico_excel.svg
+│   │   ├── ico_excel2.svg
+│   │   ├── ico_eye.svg
+│   │   ├── ico_eye2.svg
+│   │   ├── ico_gray_pin.svg
+│   │   ├── ico_grid_sort.svg
+│   │   ├── ico_grid_sort2.svg
+│   │   ├── ico_id_off.svg
+│   │   ├── ico_id_on.svg
+│   │   ├── ico_info.svg
+│   │   ├── ico_lang_english.svg
+│   │   ├── ico_lang_korea.svg
+│   │   ├── ico_lang_korea2.svg
+│   │   ├── ico_link.svg
+│   │   ├── ico_list_white.svg
+│   │   ├── ico_list.svg
+│   │   ├── ico_location_arr.svg
+│   │   ├── ico_location_home.svg
+│   │   ├── ico_logo.svg
+│   │   ├── ico_logout.svg
+│   │   ├── ico_map.svg
+│   │   ├── ico_menu_arr.svg
+│   │   ├── ico_menu_arr2.svg
+│   │   ├── ico_menu_minus.svg
+│   │   ├── ico_menu_nodata.svg
+│   │   ├── ico_menu_plus.svg
+│   │   ├── ico_menu.svg
+│   │   ├── ico_minus.svg
+│   │   ├── ico_mod_disabled.svg
+│   │   ├── ico_mod.svg
+│   │   ├── ico_mod2.svg
+│   │   ├── ico_mode_dark.svg
+│   │   ├── ico_mode_white.svg
+│   │   ├── ico_mode_white2.svg
+│   │   ├── ico_ne_add.svg
+│   │   ├── ico_ne_del_d.svg
+│   │   ├── ico_ne_del.svg
+│   │   ├── ico_no_data_nw.svg
+│   │   ├── ico_no_data.svg
+│   │   ├── ico_no_data2.svg
+│   │   ├── ico_no_table_dt.svg
+│   │   ├── ico_not_excel.svg
+│   │   ├── ico_otp_step1.svg
+│   │   ├── ico_otp_step2.svg
+│   │   ├── ico_otp_step3.svg
+│   │   ├── ico_otp_step4.svg
+│   │   ├── ico_otp_step5.svg
+│   │   ├── ico_paging_more.svg
+│   │   ├── ico_paging_next.svg
+│   │   ├── ico_paging_next1.svg
+│   │   ├── ico_paging_next2.svg
+│   │   ├── ico_paging_prev.svg
+│   │   ├── ico_paging_prev1.svg
+│   │   ├── ico_paging_prev2.svg
+│   │   ├── ico_performance1.svg
+│   │   ├── ico_performance2.svg
+│   │   ├── ico_pin_off.svg
+│   │   ├── ico_pin_on.svg
+│   │   ├── ico_pip.svg
+│   │   ├── ico_pip2.svg
+│   │   ├── ico_plus.svg
+│   │   ├── ico_pop_close.svg
+│   │   ├── ico_pos.svg
+│   │   ├── ico_ran_arrow_gray.svg
+│   │   ├── ico_ran_arrow_white.svg
+│   │   ├── ico_red_pin.svg
+│   │   ├── ico_refresh_dis.svg
+│   │   ├── ico_refresh.svg
+│   │   ├── ico_reg_disabled.svg
+│   │   ├── ico_reg.svg
+│   │   ├── ico_save_disabled.svg
+│   │   ├── ico_save.svg
+│   │   ├── ico_search.svg
+│   │   ├── ico_set_blue.svg
+│   │   ├── ico_set.svg
+│   │   ├── ico_setting.svg
+│   │   ├── ico_slt.svg
+│   │   ├── ico_slt2.svg
+│   │   ├── ico_sort.svg
+│   │   ├── ico_square.svg
+│   │   ├── ico_state1.svg
+│   │   ├── ico_state2.svg
+│   │   ├── ico_state3.svg
+│   │   ├── ico_status1.svg
+│   │   ├── ico_status2.svg
+│   │   ├── ico_status3.svg
+│   │   ├── ico_step_arr.svg
+│   │   ├── ico_step_arr2.svg
+│   │   ├── ico_tenant1.svg
+│   │   ├── ico_tenant2.svg
+│   │   ├── ico_tenant3.svg
+│   │   ├── ico_tenant4.svg
+│   │   ├── ico_time_disabled.svg
+│   │   ├── ico_time.svg
+│   │   ├── ico_tit_arr.svg
+│   │   ├── ico_tool.svg
+│   │   ├── ico_trash_nw.svg
+│   │   ├── ico_tree_add.svg
+│   │   ├── ico_tree_arr.svg
+│   │   ├── ico_tree_save.svg
+│   │   ├── ico_tree1.svg
+│   │   ├── ico_tree2.svg
+│   │   ├── ico_tree3_core.svg
+│   │   ├── ico_tree3_ran.svg
+│   │   ├── ico_tree3.svg
+│   │   ├── ico_trend.svg
+│   │   ├── ico_view_del.svg
+│   │   ├── ico_view_list.svg
+│   │   ├── ico_view_list2.svg
+│   │   ├── ico_wifi.svg
+│   │   ├── ico-arrow-right.svg
+│   │   ├── ico-check-on.svg
+│   │   ├── img_mode_dark.svg
+│   │   ├── img_mode_white.svg
+│   │   ├── img_popup.svg
+│   │   ├── img_qr.svg
+│   │   ├── img_system.svg
+│   │   ├── inf_bg.png
+│   │   ├── is_disconnect.svg
+│   │   ├── logo_foot.svg
+│   │   ├── logo_foot2.svg
+│   │   ├── logo_login.svg
+│   │   ├── logo_new.svg
+│   │   ├── logo_sams_sds.svg
+│   │   ├── logo_sams.svg
+│   │   ├── mail_logo1.png
+│   │   ├── mail_logo2.png
+│   │   ├── map_kangwon.svg
+│   │   ├── pf_sample.svg
+│   │   ├── pin.png
+│   │   ├── rlt_bg.png
+│   │   ├── round.png
+│   │   └── ven_bg.png
+│   └── scss
+│       ├── default.scss
+│       ├── main.scss
+│       ├── mode-w-m.scss
+│       ├── roulette.scss
+│       ├── sample.scss
+│       └── style.scss
+├── components
+│   ├── cellRenderer
+│   │   ├── customActionTypeTextColor.vue
+│   │   ├── customBackUpBtn.vue
+│   │   ├── customBackUpBtnR.vue
+│   │   ├── customButtonSms.vue
+│   │   ├── customHeaderText.vue
+│   │   ├── customInhibitSelect.vue
+│   │   ├── customIpConnTextColor.vue
+│   │   ├── customIpNotConnTextColor.vue
+│   │   ├── customLicenseBtn.vue
+│   │   ├── customLogLevelSelect.vue
+│   │   ├── customNullValue.vue
+│   │   ├── customRadio.vue
+│   │   ├── customResultTextDivBg.vue
+│   │   ├── customSessionSetTextField.vue
+│   │   ├── customStatusBox.vue
+│   │   ├── customTextColor.vue
+│   │   ├── customTextDivSession.vue
+│   │   └── customUseYNTextColor.vue
+│   ├── common
+│   │   ├── confirmDialog.vue
+│   │   ├── customLoading.vue
+│   │   ├── excelUpload.vue
+│   │   ├── footer
+│   │   │   └── eventDetailView.vue
+│   │   ├── footer.vue
+│   │   ├── header
+│   │   │   └── modal
+│   │   │       ├── myInfoUpdate.vue
+│   │   │       ├── passwordCheck.vue
+│   │   │       └── privacyPop.vue
+│   │   ├── header.vue
+│   │   ├── leftMenu.vue
+│   │   ├── location.vue
+│   │   ├── pagination.vue
+│   │   ├── topologyPop.vue
+│   │   └── topologyPopMgmt.vue
+│   ├── home
+│   │   ├── dashboard
+│   │   │   ├── common
+│   │   │   │   ├── coreDetailModal.vue
+│   │   │   │   ├── map
+│   │   │   │   │   ├── mapBusan.vue
+│   │   │   │   │   ├── mapChungbuk.vue
+│   │   │   │   │   ├── mapChungnam.vue
+│   │   │   │   │   ├── mapDaegu.vue
+│   │   │   │   │   ├── mapDaejeon.vue
+│   │   │   │   │   ├── mapGwangju.vue
+│   │   │   │   │   ├── mapGyeongbuk.vue
+│   │   │   │   │   ├── mapGyeonggido.vue
+│   │   │   │   │   ├── mapGyeongnam.vue
+│   │   │   │   │   ├── mapIncheon.vue
+│   │   │   │   │   ├── mapJeju.vue
+│   │   │   │   │   ├── mapJeonbuk.vue
+│   │   │   │   │   ├── mapJeonnam.vue
+│   │   │   │   │   ├── mapKangwon.vue
+│   │   │   │   │   ├── mapSejong.vue
+│   │   │   │   │   ├── mapSeoul.vue
+│   │   │   │   │   └── mapUlsan.vue
+│   │   │   │   ├── pagination.vue
+│   │   │   │   ├── ranCardGroupDetailModal.vue
+│   │   │   │   ├── ranMapGroupDetailModal.vue
+│   │   │   │   └── ranMapNeDetailModal.vue
+│   │   │   ├── layout01
+│   │   │   │   ├── core
+│   │   │   │   │   ├── layout01Core.vue
+│   │   │   │   │   ├── layout01CoreWidgetM.vue
+│   │   │   │   │   └── layout01CoreWidgetS.vue
+│   │   │   │   ├── layout01.vue
+│   │   │   │   ├── ran
+│   │   │   │   │   └── layout01Ran.vue
+│   │   │   │   └── user
+│   │   │   │       ├── layout01User.vue
+│   │   │   │       ├── layout01UserWidgetM.vue
+│   │   │   │       ├── layout01UserWidgetS.vue
+│   │   │   │       └── layout01UserWidgetT.vue
+│   │   │   ├── layout02
+│   │   │   │   ├── core
+│   │   │   │   │   ├── layout02Core.vue
+│   │   │   │   │   ├── layout02CoreWidgetM.vue
+│   │   │   │   │   └── layout02CoreWidgetS.vue
+│   │   │   │   ├── layout02.vue
+│   │   │   │   ├── ran
+│   │   │   │   │   └── layout02Ran.vue
+│   │   │   │   └── user
+│   │   │   │       ├── layout02User.vue
+│   │   │   │       ├── layout02UserWidgetM.vue
+│   │   │   │       ├── layout02UserWidgetS.vue
+│   │   │   │       └── layout02UserWidgetT.vue
+│   │   │   ├── layout03
+│   │   │   │   ├── core
+│   │   │   │   │   ├── layout03Core.vue
+│   │   │   │   │   ├── layout03CoreWidgetM.vue
+│   │   │   │   │   └── layout03CoreWidgetS.vue
+│   │   │   │   ├── layout03.vue
+│   │   │   │   ├── ran
+│   │   │   │   │   ├── layout03Ran.vue
+│   │   │   │   │   └── ranMapComponent.vue
+│   │   │   │   └── user
+│   │   │   │       ├── layout03User.vue
+│   │   │   │       ├── layout03UserWidgetM.vue
+│   │   │   │       └── layout03UserWidgetS.vue
+│   │   │   ├── settingModal.vue
+│   │   │   └── test.json
+│   │   ├── jobNoti
+│   │   │   └── jobNotiModal.vue
+│   │   ├── tenant
+│   │   │   ├── chart
+│   │   │   │   ├── doughnut.vue
+│   │   │   │   ├── trendBar.vue
+│   │   │   │   └── userDoughnut.vue
+│   │   │   ├── common
+│   │   │   │   └── ranGroupDetailModal.vue
+│   │   │   ├── tenantRan.vue
+│   │   │   ├── tenantTrend.vue
+│   │   │   └── tenantUser.vue
+│   │   └── trend
+│   │       └── headerChart.vue
+│   ├── login
+│   │   └── privacyPop.vue
+│   ├── search
+│   │   └── searchModules.vue
+│   └── sunEdt.vue
+├── composables
+│   ├── useApi.js
+│   ├── useAxios.js
+│   ├── useChart.js
+│   ├── useEnumCode.js
+│   ├── useEnumCodeEn.js
+│   ├── useEnumCodeKr.js
+│   ├── useErrorHandler.js
+│   ├── useHangul.js
+│   ├── useMenuConstants.js
+│   ├── useSunEditor.js
+│   ├── useToastEditor.ts
+│   ├── useUrlHandler.js
+│   ├── useUtil.js
+│   ├── useValid.js
+│   └── useWatchFocusValidate.js
+├── error.vue
+├── lang
+│   ├── en.js
+│   └── kr.js
+├── layouts
+│   ├── default.vue
+│   ├── designdefault.vue
+│   ├── designloginlayout.vue
+│   ├── loginlayout.vue
+│   ├── roulette.vue
+│   └── samplelayout.vue
+├── middleware
+│   └── auth.global.js
+├── nuxt.config.ts
+├── package-lock.json
+├── package.json
+├── pages
+│   ├── auth
+│   │   ├── join.vue
+│   │   └── popupClose.vue
+│   ├── index.vue
+│   └── view
+│       ├── cs
+│       │   ├── financial.vue
+│       │   └── index.vue
+│       ├── deli
+│       │   ├── index.vue
+│       │   ├── mngAdd.vue
+│       │   └── mngListDeleted.vue
+│       ├── item
+│       │   ├── add.vue
+│       │   ├── evtListClosed.vue
+│       │   ├── evtListOngoing.vue
+│       │   ├── evtListPending.vue
+│       │   └── index.vue
+│       ├── log
+│       │   └── logList.vue
+│       ├── order
+│       │   └── index.vue
+│       ├── settle
+│       │   ├── curationAdd.vue
+│       │   ├── curationList.vue
+│       │   ├── index.vue
+│       │   ├── irAdd.vue
+│       │   ├── mediaAdd.vue
+│       │   ├── mediaList.vue
+│       │   ├── newsAdd.vue
+│       │   └── newsList.vue
+│       └── vendor
+│           ├── dashboard
+│           │   └── index.vue
+│           └── index.vue
+├── plugins
+│   ├── fontawesome.js
+│   ├── i18n.js
+│   ├── log.js
+│   ├── mitt.js
+│   ├── toast.js
+│   ├── userAgent.js
+│   ├── vue-cool-lightbox.js
+│   ├── vue3-editor.js
+│   └── vuetify.js
+├── public
+│   ├── favicon.ico
+│   ├── ft_logo.png
+│   ├── js
+│   │   └── jquery-3.7.1.min.js
+│   └── logo.png
+├── README.md
+├── server
+│   └── tsconfig.json
+├── stores
+│   ├── auth.js
+│   ├── detail.js
+│   ├── lang.js
+│   ├── loading.js
+│   └── tenantMgmt.js
+├── toast-editor.d.ts
+├── tsconfig.json
+└── vite-plugin-sri.d.ts
+```
+
+### Data Flow & Communication Patterns
+- **Client-Server 통신**: RESTful API, JWT 인증 헤더, Axios 인터셉터  
+- **Database 상호작용**: CI4 Query Builder/Model, 트랜잭션 관리, Redis 캐시 사용  
+- **외부 서비스 연동**: 비동기 메시지 큐 없이 HTTP 호출, 에러 리트라이 로직  
+- **실시간 통신**: Socket.IO 기반 WebSocket 연결, 주문·승인 알림  
+- **데이터 동기화**: 캐시 무효화 패턴, 이벤트 기반 상태 업데이트  
+
+## 4. Performance & Optimization Strategy
+- HTTP 응답 캐싱: Redis로 빈번 조회 데이터 캐싱  
+- DB 인덱싱 및 쿼리 튜닝: 주요 조회 쿼리 Explain 분석  
+- 코드 스플리팅·지연 로딩: Nuxt3 동적 import 활용  
+- 로드 밸런싱: Kubernetes HPA 기반 자동 스케일링  
+
+## 5. Implementation Roadmap & Milestones
+
+### Phase 1: Foundation (MVP Implementation)
+- Core Infrastructure: Docker/K8s 환경, CI/CD 파이프라인  
+- Essential Features: 로그인·회원가입, 상품 조회·발주, 주문 승인, 송장 엑셀 업로드  
+- Basic Security: JWT 인증, HTTPS, OAuth2 SNS 로그인  
+- Development Setup: 로컬 개발 환경, 코드 린팅·테스트 프레임워크  
+- Timeline: M+2
+
+### Phase 2: Feature Enhancement
+- Advanced Features: 정산 모듈, 파트너 매칭 시스템, 알림 센터  
+- Performance Optimization: 캐시 전략, DB 튜닝  
+- Enhanced Security: 권한 관리 강화, OWASP 점검  
+- Monitoring Implementation: ELK 대시보드, Grafana 알림  
+- Timeline: M+4
+
+### Phase 3: Scaling & Optimization
+- Scalability Implementation: HPA/Cluster Autoscaler, DB 리드 리플리카  
+- Advanced Integrations: ERP 연동, 다중 택배사 API 연결  
+- Enterprise Features: 서브계정 관리, 대시보드  
+- Compliance & Auditing: GDPR, 데이터 암호화 심화  
+- Timeline: M+6
+
+## 6. Risk Assessment & Mitigation Strategies
+
+### Technical Risk Analysis
+- **기술 리스크**: OCR 인식률 저하 → 수동 검증 UI 제공  
+- **성능 리스크**: 동시 사용자 증가 시 DB 병목 → 읽기/쓰기 분리, 캐시 활용  
+- **보안 리스크**: 토큰 탈취 → 짧은 만료, 리프레시 토큰 설계  
+- **통합 리스크**: 외부 API 변경 → 버전 관리, 어댑터 패턴 적용  
+- **Mitigation**: 대체 흐름, 로깅·모니터링 알림, 자동 테스트
+
+### Project Delivery Risks
+- **일정 리스크**: 기능 지연 → MVP 단계별 우선순위 조정  
+- **자원 리스크**: 전문 인력 부족 → 외부 컨설팅·아웃소싱 검토  
+- **품질 리스크**: 테스트 커버리지 부족 → CI/CD 자동화 테스트 강화  
+- **배포 리스크**: 프로덕션 오류 → 블루/그린 배포 전략 채택  
+- **Contingency**: 페이즈별 핵심 기능 최소화, 백업 환경 준비  
+
+---  
+*본 문서는 PRD 기반 최소 기능 중심으로 설계되었으며, 차후 요구사항 변화에 따라 단계별 확장이 가능합니다.*

+ 110 - 0
.cursor/rules/vooster__clean-code.mdc

@@ -0,0 +1,110 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+
+# Clean Code Guidelines
+
+You are an expert software engineer focused on writing clean, maintainable code. Follow these principles rigorously:
+
+## Core Principles
+- **DRY** - Eliminate duplication ruthlessly
+- **KISS** - Simplest solution that works
+- **YAGNI** - Build only what's needed now
+- **SOLID** - Apply all five principles consistently
+- **Boy Scout Rule** - Leave code cleaner than found
+
+## Naming Conventions
+- Use **intention-revealing** names
+- Avoid abbreviations except well-known ones (e.g., URL, API)
+- Classes: **nouns**, Methods: **verbs**, Booleans: **is/has/can** prefix
+- Constants: UPPER_SNAKE_CASE
+- No magic numbers - use named constants
+
+## Functions & Methods
+- **Single Responsibility** - one reason to change
+- Maximum 20 lines (prefer under 10)
+- Maximum 3 parameters (use objects for more)
+- No side effects in pure functions
+- Early returns over nested conditions
+
+## Code Structure
+- **Cyclomatic complexity** < 10
+- Maximum nesting depth: 3 levels
+- Organize by feature, not by type
+- Dependencies point inward (Clean Architecture)
+- Interfaces over implementations
+
+## Comments & Documentation
+- Code should be self-documenting
+- Comments explain **why**, not what
+- Update comments with code changes
+- Delete commented-out code immediately
+- Document public APIs thoroughly
+
+## Error Handling
+- Fail fast with clear messages
+- Use exceptions over error codes
+- Handle errors at appropriate levels
+- Never catch generic exceptions
+- Log errors with context
+
+## Testing
+- **TDD** when possible
+- Test behavior, not implementation
+- One assertion per test
+- Descriptive test names: `should_X_when_Y`
+- **AAA pattern**: Arrange, Act, Assert
+- Maintain test coverage > 80%
+
+## Performance & Optimization
+- Profile before optimizing
+- Optimize algorithms before micro-optimizations
+- Cache expensive operations
+- Lazy load when appropriate
+- Avoid premature optimization
+
+## Security
+- Never trust user input
+- Sanitize all inputs
+- Use parameterized queries
+- Follow **principle of least privilege**
+- Keep dependencies updated
+- No secrets in code
+
+## Version Control
+- Atomic commits - one logical change
+- Imperative mood commit messages
+- Reference issue numbers
+- Branch names: `type/description`
+- Rebase feature branches before merging
+
+## Code Reviews
+- Review for correctness first
+- Check edge cases
+- Verify naming clarity
+- Ensure consistent style
+- Suggest improvements constructively
+
+## Refactoring Triggers
+- Duplicate code (Rule of Three)
+- Long methods/classes
+- Feature envy
+- Data clumps
+- Divergent change
+- Shotgun surgery
+
+## Final Checklist
+Before committing, ensure:
+- [ ] All tests pass
+- [ ] No linting errors
+- [ ] No console logs
+- [ ] No commented code
+- [ ] No TODOs without tickets
+- [ ] Performance acceptable
+- [ ] Security considered
+- [ ] Documentation updated
+
+Remember: **Clean code reads like well-written prose**. Optimize for readability and maintainability over cleverness.
+

+ 302 - 0
.cursor/rules/vooster__guideline.mdc

@@ -0,0 +1,302 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+# Code Guidelines for Influencer–Vendor Automation Platform
+
+## 1. Project Overview  
+A unified web platform to automate ordering, shipping, settlement, and notifications between influencers and vendors.  
+Key architectural decisions:  
+- Frontend: Vue 3 + Nuxt 3 (SSR/SSG), Composition API, TypeScript, Pinia, Vuetify, Axios  
+- BFF: Node.js + Express (ES Modules), Socket.IO for real-time  
+- Backend API: CodeIgniter 4 RESTful controllers, MySQL (RDS), Redis cache  
+- Deployment: Docker → Kubernetes, CI/CD via GitHub Actions  
+- Integrations: Google Cloud Vision OCR, Courier & ERP REST APIs, JWT/OAuth2 authentication  
+
+---
+
+## 2. Core Principles  
+1. Single Responsibility: each function/module addresses one concern; max 200 lines.  
+2. Strong Typing: avoid `any`; define interfaces for props, DTOs, API responses.  
+3. Consistent Error Handling: centralize and standardize error responses and logs.  
+4. DRY & Reusable: extract shared logic into composables, services, or utilities.  
+5. Domain-Driven Modules: group files by business domain (order, shipping, finance).  
+
+---
+
+## 3. Language-Specific Guidelines  
+
+### 3.1 Vue 3 + Nuxt 3 + TypeScript  
+- File Organization:  
+  - `/pages` → route pages  
+  - `/components/{domain}` → feature components  
+  - `/composables` → reusable logic hooks (prefixed `useXxx`)  
+  - `/stores/{domain}` → Pinia modules (one per domain)  
+  - `/plugins`, `/middleware`, `/assets`, `/layouts`  
+- Imports & Aliases:  
+  - Use Nuxt aliases: `import X from '~/components/order/OrderList.vue'`  
+  - Group imports: external packages → aliased aliases → relative (sorted alphabetically)  
+- Error Handling:  
+  - Global error plugin `~/plugins/error.ts` to catch and display Axios errors  
+  - In composable:  
+    ```ts
+    export async function useFetchOrders() {
+      try {
+        const { data } = await $axios.get<Order[]>('/api/orders')
+        return data
+      } catch (error: unknown) {
+        throw new ApiError(error)
+      }
+    }
+    ```  
+
+### 3.2 Node.js + Express (BFF)  
+- Folder Structure:  
+  ```
+  /src
+    /controllers
+    /services
+    /routes
+    /middlewares
+    /utils
+    app.js
+  ```
+- Dependency Management:  
+  - Use ES Modules (`"type": "module"`) or TypeScript.  
+  - Version-lock in `package.json`; run `npm audit` in CI.  
+- Error Handling:  
+  - Create `HttpError` class in `/utils/HttpError.js`  
+  - Middleware `errorHandler.js` at the end:  
+    ```js
+    app.use((err, req, res, next) => {
+      logger.error(err)
+      res.status(err.statusCode || 500).json({
+        success: false,
+        message: err.message || 'Internal Server Error'
+      })
+    })
+    ```  
+
+### 3.3 CodeIgniter 4 (REST API)  
+- Controllers: one per resource, extend `ResourceController`  
+- Models: use Entities and Query Builder; keep business logic in Services  
+- Validation & Responses:  
+  ```php
+  public function create()
+  {
+    $rules = ['order_id' => 'required|integer', /* ... */];
+    if (! $this->validate($rules)) {
+      return $this->fail($this->validator->getErrors());
+    }
+    $entity = new OrderEntity($this->request->getPost());
+    $this->orderService->save($entity);
+    return $this->respondCreated($entity);
+  }
+  ```
+- Error Handling: use `HTTPException` for 404/403, global logging in `app/Filters`.  
+
+---
+
+## 4. Code Style Rules  
+
+### 4.1 MUST Follow  
+- **Use Strict Typescript**  
+  Rationale: catch errors at compile time.  
+  ```jsonc
+  // tsconfig.json
+  {
+    "compilerOptions": {
+      "strict": true,
+      "noImplicitAny": true,
+      "forceConsistentCasingInFileNames": true
+    }
+  }
+  ```  
+- **One Component per File**  
+  Rationale: clarity, reusability, smaller diffs.  
+- **Composition API & `<script setup>`**  
+  Rationale: simpler syntax and tree-shaking.  
+  ```vue
+  <script setup lang="ts">
+  import { ref } from 'vue'
+  const count = ref(0)
+  function increment() { count.value++ }
+  </script>
+  ```  
+- **Pinia Stores for State**  
+  Rationale: predictable global state with actions, getters.  
+  ```ts
+  import { defineStore } from 'pinia'
+  export const useOrderStore = defineStore('order', {
+    state: () => ({ list: [] as Order[] }),
+    actions: {
+      async fetch() { this.list = await fetchOrders() }
+    }
+  })
+  ```  
+- **RESTful API Design**  
+  Rationale: consistency and predictability.  
+  - Use resource paths: `/vendors/{id}/orders`  
+  - HTTP verbs: GET/POST/PUT/DELETE  
+  - Standard response envelope:  
+    ```json
+    { "success": true, "data": {...}, "error": null }
+    ```  
+- **Centralized Error Handler**  
+  Rationale: unified logging and client messages.  
+
+### 4.2 MUST NOT Do  
+- **Avoid `any` or disabling lint rules**  
+  Rationale: loses type-safety.  
+- **No Large “God” Modules**  
+  Rationale: hard to test and maintain.  
+- **No Inline Styles or Scripts**  
+  Rationale: separates concerns; use Vuetify theme or SCSS.  
+- **No Nested Callbacks (Callback Hell)**  
+  Rationale: use async/await or Promises.  
+- **No Direct DOM Manipulation**  
+  Rationale: Vue manages DOM; use refs or directives.  
+
+---
+
+## 5. Architecture Patterns  
+
+### 5.1 Component & Module Structure  
+- Domain-Driven Folders:  
+  ```
+  /components/order
+  /components/shipping
+  /composables/order
+  /stores/order
+  /services/api/order.ts
+  ```  
+- Layers in BFF:  
+  - **Routes** → **Controllers** → **Services** → **Data Access**  
+
+### 5.2 Data Flow  
+- **Frontend**: Props ↓, Events ↑, Store (Pinia) for shared state, Composables for side-effects.  
+- **API Calls**: Axios interceptors attach JWT, handle 401 globally, retry logic for idempotent GETs.  
+- **Real-time**: Socket.IO client in plugin; update Pinia store on events.  
+
+### 5.3 State Management  
+- Local state in component for UI-only values (`ref`, `reactive`).  
+- Global state in Pinia: one store per domain; expose typed actions/getters.  
+- Keep store actions async, commit minimal state changes.  
+
+### 5.4 API Design Standards  
+- Base URL per domain: `/api/v1/orders`, `/api/v1/vendors`  
+- Pagination: standard query `?page=1&limit=20`, return `{ items, total, page, limit }`.  
+- Filtering & Sorting: query params `?status=shipped&sort=-date`.  
+- Consistent Error Payload:  
+  ```json
+  {
+    "success": false,
+    "error": {
+      "code": "VALIDATION_FAILED",
+      "message": "Invalid field: quantity"
+    }
+  }
+  ```  
+
+---
+
+## 6. Example Code Snippets  
+
+### 6.1 Vue Composition & API Call  
+```ts
+// MUST: composable with typed response and error handling
+import { ref } from 'vue'
+import { Order } from '~/types'
+import { useApi } from '~/composables/useApi'
+
+export function useOrders() {
+  const list = ref<Order[]>([])
+  const error = ref<string | null>(null)
+  async function fetchOrders() {
+    try {
+      const res = await useApi().get<Order[]>('/orders')
+      list.value = res.data
+    } catch (e) {
+      error.value = e.message
+    }
+  }
+  return { list, error, fetchOrders }
+}
+```
+
+```ts
+// MUST NOT: direct Axios calls in component, untyped any
+setup() {
+  axios.get('/orders').then(res => {
+    this.orders = res.data
+  })
+}
+```
+
+### 6.2 Node.js Express Route & Error  
+```js
+// MUST: clean controller and error propagation
+// src/routes/order.js
+import { Router } from 'express'
+import { listOrders } from '../controllers/order.js'
+const router = Router()
+router.get('/', listOrders)
+export default router
+
+// src/controllers/order.js
+export async function listOrders(req, res, next) {
+  try {
+    const orders = await OrderService.fetchAll()
+    res.json({ success: true, data: orders })
+  } catch (err) {
+    next(new HttpError(500, 'Failed to fetch orders'))
+  }
+}
+```
+
+```js
+// MUST NOT: catch without forwarding or unstructured response
+app.get('/orders', async (req, res) => {
+  try {
+    const orders = await OrderService.fetchAll()
+    res.send(orders)
+  } catch (err) {
+    res.status(500).send('Error')
+  }
+})
+```
+
+### 6.3 CodeIgniter4 Controller  
+```php
+// MUST: validate, use entity, consistent response
+class OrderController extends ResourceController
+{
+  public function create()
+  {
+    $rules = ['vendor_id'=>'required|integer', 'items'=>'required|array'];
+    if (! $this->validate($rules)) {
+      return $this->failValidationErrors($this->validator->getErrors());
+    }
+    $order = new OrderEntity($this->request->getPost());
+    $this->orderService->create($order);
+    return $this->respondCreated(['order' => $order]);
+  }
+}
+```
+
+```php
+// MUST NOT: raw SQL in controller, no validation
+class OrderController extends BaseController
+{
+  public function create()
+  {
+    $db->query("INSERT INTO orders ..."); // anti-pattern
+  }
+}
+```
+
+---
+
+End of Guidelines.  
+Follow these rules as the single source of truth for code quality, maintainability, and consistency.

+ 102 - 0
.cursor/rules/vooster__prd.mdc

@@ -0,0 +1,102 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+# 제품 요구사항 문서 (PRD)
+
+## 1. 개요
+인플루언서와 벤더사가 문서 교환 없이 웹 기반으로 수·발주 및 정산을 수행할 수 있는 통합 플랫폼 구축. 배송·정산·고객응대 전 과정을 자동화해 거래 속도와 신뢰도를 향상시킨다.
+
+## 2. 문제 정의
+- 엑셀·PDF 등 오프라인 문서 교환으로 인한 오류·지연
+- 송장번호 수기 입력, 재고·배송 상태 불일치
+- 계약·정산 내역 확인이 어렵고 가시성 부족
+- 인플루언서 파트너 탐색 과정의 비효율
+
+## 3. 목표 및 목적
+- 1차 목표: 발주·승인·송장·정산 전 프로세스 웹 자동화
+- 2차 목표: 파트너 매칭, 서브계정 관리, OCR 기반 송장 인식
+- 성공지표
+  - 발주 승인 평균 소요시간 50% 단축
+  - 송장 입력 오류율 90% ↓
+  - 월간 활성 인플루언서 수 1,000명
+  - 거래액 월 10억 원
+
+## 4. 타깃 사용자
+### 주요 사용자
+- 인플루언서: SNS 영향력 보유, 공동구매 운영, 재고·배송 부담 최소화 희망
+- 벤더사: 상품 공급, 재고·배송·정산 자동화 필요
+### 부차 사용자
+- 통합 벤더사/유통업체, CS 담당자, 회계팀
+
+## 5. 사용자 스토리
+- “인플루언서로서 벤더사 제품을 조회·발주해 판매 준비를 간편히 하고 싶다.”
+- “벤더사 담당자로서 인플루언서 요청을 클릭 한 번에 승인·거절하고 싶다.”
+- “CS 담당자로서 주문 상태를 실시간 파악해 문의에 즉시 대응하고 싶다.”
+- “벤더사 마스터로서 서브계정을 생성해 팀별 권한을 차등 부여하고 싶다.”
+
+## 6. 기능 요구사항
+### 핵심 기능
+1. 벤더사 포털  
+   - 상품 등록/수정/상태관리(배송중, 품절 등)  
+     Acceptance: 상품 등록 시 필수 필드 검증, 상태 변경시 실시간 알림  
+   - 주문 관리: 인플루언서 수주 승인/거절, 일괄처리  
+   - 배송 관리  
+     - 송장번호 엑셀 업로드·다운로드  
+     - 송장 사진 OCR → 자동 입력  
+   - 정산 관리: 월간 계약·정산 내역 확인, CSV 다운로드  
+   - 인플루언서 승인 및 제안: 가입 요청 승인 및 파트너 제안 발송  
+2. 인플루언서 포털  
+   - SNS 간편 로그인(Google, Kakao, Naver)  
+   - 벤더사 가입 요청 및 제안 수락  
+   - 연결된 벤더사 상품 리스트 조회  
+   - 상품 수주 요청, 송장번호 엑셀 다운로드  
+3. 매칭 시스템  
+   - 벤더사 조건(카테고리·팔로워 수) 기반 추천 알고리즘  
+4. 알림 센터  
+   - 이메일·푸시·웹 소켓 알림: 주문 상태, 승인 결과, 정산 완료  
+### 보조 기능
+- 벤더사 서브계정 관리(역할·권한 설정)
+- 고객센터 게시판(FAQ, 1:1 문의)
+- 대시보드(매출, 주문, 정산 현황)
+
+## 7. 비기능 요구사항
+- 성능: 평균 응답 300ms 이하, 동시 5,000사용자
+- 보안: JWT 인증, OAuth2 SNS 로그인, HTTPS, 데이터 암호화
+- 사용성: 반응형 UI, 접근성 WCAG 2.1 AA
+- 확장성: 모듈화된 Micro Frontend, RESTful API
+- 호환성: 최신 크롬·사파리·엣지, 모바일 브라우저
+
+## 8. 기술 고려사항
+- 프론트엔드: Vue3 + Composition API, Nuxt3, Vuetify, TypeScript, Axios
+- 백엔드: CodeIgniter4 + Node.js BFF
+- DB: MySQL(RDS), Redis 캐시
+- 배포: Docker, K8s, GitHub Actions CI/CD
+- OCR: Google Cloud Vision API
+- 통합: 택배사 API(송장 추적), 회계 시스템 ERP 연동
+- 로깅·모니터링: ELK, Grafana
+
+## 9. 성공 지표(KPI)
+- DAU / MAU, 유입·활성률
+- 주문 승인 건수, 배송 완료율
+- 거래 총액, 재구매율
+- 서버 오류율 <1%, 가용성 99.9%
+
+## 10. 일정 및 마일스톤
+- Phase 1 (M+2): 로그인, 상품·주문·배송 기본, MVP 런칭
+- Phase 2 (M+4): 정산 모듈, 매칭 시스템, 알림 센터
+- Phase 3 (M+6): OCR, 서브계정, 대시보드, 모바일 최적화
+- Phase 4 (M+9): AI 수요 예측, 글로벌 배송 연동
+
+## 11. 위험 및 대응
+- 데이터 정확도: OCR 오류 → 수동 검증 UI 제공
+- 사용 미정착: 온보딩 튜토리얼, FAQ 운영
+- 벤더사 API 변경: 버전 관리, 어댑터 패턴
+- 개인정보 유출: 정기 보안 점검, 침투 테스트
+
+## 12. 향후 고려사항
+- 인플루언서 커미션 자동 분배, 블록체인 정산
+- 다국어 지원, 환율 자동 적용
+- 모바일 앱(iOS/Android) 출시
+- AI 기반 악성 주문 탐지, 인플루언서 등급제 도입

+ 99 - 0
.cursor/rules/vooster__step-by-step.mdc

@@ -0,0 +1,99 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+
+## Core Directive
+You are a senior software engineer AI assistant. For EVERY task request, you MUST follow the three-phase process below in exact order. Each phase must be completed with expert-level precision and detail.
+
+## Guiding Principles
+- **Minimalistic Approach**: Implement high-quality, clean solutions while avoiding unnecessary complexity
+- **Expert-Level Standards**: Every output must meet professional software engineering standards
+- **Concrete Results**: Provide specific, actionable details at each step
+
+---
+
+## Phase 1: Codebase Exploration & Analysis
+**REQUIRED ACTIONS:**
+1. **Systematic File Discovery**
+   - List ALL potentially relevant files, directories, and modules
+   - Search for related keywords, functions, classes, and patterns
+   - Examine each identified file thoroughly
+
+2. **Convention & Style Analysis**
+   - Document coding conventions (naming, formatting, architecture patterns)
+   - Identify existing code style guidelines
+   - Note framework/library usage patterns
+   - Catalog error handling approaches
+
+**OUTPUT FORMAT:**
+```
+### Codebase Analysis Results
+**Relevant Files Found:**
+- [file_path]: [brief description of relevance]
+
+**Code Conventions Identified:**
+- Naming: [convention details]
+- Architecture: [pattern details]
+- Styling: [format details]
+
+**Key Dependencies & Patterns:**
+- [library/framework]: [usage pattern]
+```
+
+---
+
+## Phase 2: Implementation Planning
+**REQUIRED ACTIONS:**
+Based on Phase 1 findings, create a detailed implementation roadmap.
+
+**OUTPUT FORMAT:**
+```markdown
+## Implementation Plan
+
+### Module: [Module Name]
+**Summary:** [1-2 sentence description of what needs to be implemented]
+
+**Tasks:**
+- [ ] [Specific implementation task]
+- [ ] [Specific implementation task]
+
+**Acceptance Criteria:**
+- [ ] [Measurable success criterion]
+- [ ] [Measurable success criterion]
+- [ ] [Performance/quality requirement]
+
+### Module: [Next Module Name]
+[Repeat structure above]
+```
+
+---
+
+## Phase 3: Implementation Execution
+**REQUIRED ACTIONS:**
+1. Implement each module following the plan from Phase 2
+2. Verify ALL acceptance criteria are met before proceeding
+3. Ensure code adheres to conventions identified in Phase 1
+
+**QUALITY GATES:**
+- [ ] All acceptance criteria validated
+- [ ] Code follows established conventions
+- [ ] Minimalistic approach maintained
+- [ ] Expert-level implementation standards met
+
+---
+
+## Success Validation
+Before completing any task, confirm:
+- ✅ All three phases completed sequentially
+- ✅ Each phase output meets specified format requirements
+- ✅ Implementation satisfies all acceptance criteria
+- ✅ Code quality meets professional standards
+
+## Response Structure
+Always structure your response as:
+1. **Phase 1 Results**: [Codebase analysis findings]
+2. **Phase 2 Plan**: [Implementation roadmap]  
+3. **Phase 3 Implementation**: [Actual code with validation]
+

+ 101 - 0
.cursor/rules/vooster__tdd.mdc

@@ -0,0 +1,101 @@
+---
+description:
+globs:
+alwaysApply: true
+---
+
+# TDD Process Guidelines - Cursor Rules
+
+## ⚠️ MANDATORY: Follow these rules for EVERY implementation and modification
+
+**This document defines the REQUIRED process for all code changes. No exceptions without explicit team approval.**
+
+## Core Cycle: Red → Green → Refactor
+
+### 1. RED Phase
+- Write a failing test FIRST
+- Test the simplest scenario
+- Verify test fails for the right reason
+- One test at a time
+
+### 2. GREEN Phase  
+- Write MINIMAL code to pass
+- "Fake it till you make it" is OK
+
+- YAGNI principle
+
+### 3. REFACTOR Phase
+- Remove duplication
+- Improve naming
+- Simplify structure
+- Keep tests passing
+
+## Test Quality: FIRST Principles
+- **Fast**: Milliseconds, not seconds
+- **Independent**: No shared state
+- **Repeatable**: Same result every time
+- **Self-validating**: Pass/fail, no manual checks
+- **Timely**: Written just before code
+
+## Test Structure: AAA Pattern
+```
+// Arrange
+Set up test data and dependencies
+
+// Act
+Execute the function/method
+
+// Assert
+Verify expected outcome
+```
+
+## Implementation Flow
+1. **List scenarios** before coding
+2. **Pick one scenario** → Write test
+3. **Run test** → See it fail (Red)
+4. **Implement** → Make it pass (Green)
+5. **Refactor** → Clean up (Still Green)
+6. **Commit** → Small, frequent commits
+7. **Repeat** → Next scenario
+
+## Test Pyramid Strategy
+- **Unit Tests** (70%): Fast, isolated, numerous
+- **Integration Tests** (20%): Module boundaries
+- **Acceptance Tests** (10%): User scenarios
+
+## Outside-In vs Inside-Out
+- **Outside-In**: Start with user-facing test → Mock internals → Implement details
+- **Inside-Out**: Start with core logic → Build outward → Integrate components
+
+## Common Anti-patterns to Avoid
+- Testing implementation details
+- Fragile tests tied to internals  
+- Missing assertions
+- Slow, environment-dependent tests
+- Ignored failing tests
+
+## When Tests Fail
+1. **Identify**: Regression, flaky test, or spec change?
+2. **Isolate**: Narrow down the cause
+3. **Fix**: Code bug or test bug
+4. **Learn**: Add missing test cases
+
+## Team Practices
+- CI/CD integration mandatory
+- No merge without tests
+- Test code = Production code quality
+- Pair programming for complex tests
+- Regular test refactoring
+
+## Pragmatic Exceptions
+- UI/Graphics: Manual + snapshot tests
+- Performance: Benchmark suites
+- Exploratory: Spike then test
+- Legacy: Test on change
+
+## Remember
+- Tests are living documentation
+- Test behavior, not implementation
+- Small steps, fast feedback
+- When in doubt, write a test
+

+ 66 - 0
.cursor/rules/vue-rule.mdc

@@ -0,0 +1,66 @@
+---
+alwaysApply: true
+---
+
+
+
+  You are an expert in Laravel, Vue.js, and modern full-stack web development technologies.
+
+  Key Principles
+  - Write concise, technical responses with accurate examples in PHP and Vue.js.
+  - Follow Laravel and Vue.js best practices and conventions.
+  - Use object-oriented programming with a focus on SOLID principles.
+  - Favor iteration and modularization over duplication.
+  - Use descriptive and meaningful names for variables, methods, and files.
+  - Adhere to Laravel's directory structure conventions (e.g., app/Http/Controllers).
+  - Prioritize dependency injection and service containers.
+
+  Laravel
+  - Leverage PHP 8.2+ features (e.g., readonly properties, match expressions).
+  - Apply strict typing: declare(strict_types=1).
+  - Follow PSR-12 coding standards for PHP.
+  - Use Laravel's built-in features and helpers (e.g., `Str::` and `Arr::`).
+  - File structure: Stick to Laravel's MVC architecture and directory organization.
+  - Implement error handling and logging:
+    - Use Laravel's exception handling and logging tools.
+    - Create custom exceptions when necessary.
+    - Apply try-catch blocks for predictable errors.
+  - Use Laravel's request validation and middleware effectively.
+  - Implement Eloquent ORM for database modeling and queries.
+  - Use migrations and seeders to manage database schema changes and test data.
+
+  Vue.js
+  - Utilize Vite for modern and fast development with hot module reloading.
+  - Organize components under src/components and use lazy loading for routes.
+  - Apply Vue Router for SPA navigation and dynamic routing.
+  - Implement Pinia for state management in a modular way.
+  - Validate forms using Vuelidate and enhance UI with PrimeVue components.
+  
+  Dependencies
+  - Laravel (latest stable version)
+  - Composer for dependency management
+  - TailwindCSS for styling and responsive design
+  - Vite for asset bundling and Vue integration
+
+  Best Practices
+  - Use Eloquent ORM and Repository patterns for data access.
+  - Secure APIs with Laravel Passport and ensure proper CSRF protection.
+  - Leverage Laravel’s caching mechanisms for optimal performance.
+  - Use Laravel’s testing tools (PHPUnit, Dusk) for unit and feature testing.
+  - Apply API versioning for maintaining backward compatibility.
+  - Ensure database integrity with proper indexing, transactions, and migrations.
+  - Use Laravel's localization features for multi-language support.
+  - Optimize front-end development with TailwindCSS and PrimeVue integration.
+
+  Key Conventions
+  1. Follow Laravel's MVC architecture.
+  2. Use routing for clean URL and endpoint definitions.
+  3. Implement request validation with Form Requests.
+  4. Build reusable Vue components and modular state management.
+  5. Use Laravel's Blade engine or API resources for efficient views.
+  6. Manage database relationships using Eloquent's features.
+  7. Ensure code decoupling with Laravel's events and listeners.
+  8. Implement job queues and background tasks for better scalability.
+  9. Use Laravel's built-in scheduling for recurring processes.
+  10. Employ Laravel Mix or Vite for asset optimization and bundling.
+  

+ 6 - 0
.vooster/project.json

@@ -0,0 +1,6 @@
+{
+  "uid": "WOM0",
+  "name": "인플루언서와 벤더사간에 수발주를 원할하게 하는 시스템을 구축하려고 합니다.",
+  "description": "인플루언서와 벤더사간에 수발주를 원할하게 하는 시스템을 구축하려고 합니다.",
+  "connectedAt": "2025-07-17T02:08:34.585Z"
+}

File diff ditekan karena terlalu besar
+ 4 - 0
.vooster/rules.json


+ 156 - 0
.vooster/tasks.json

@@ -0,0 +1,156 @@
+{
+  "totalCount": 15,
+  "downloadedAt": "2025-07-21T06:42:29.250Z",
+  "tasks": [
+    {
+      "taskId": "T-001",
+      "summary": "공통 인증 및 권한 관리 모듈 설계 및 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 5,
+      "urgency": 7,
+      "createdAt": "2025-07-17T02:02:42.157Z",
+      "updatedAt": "2025-07-17T02:02:42.157Z"
+    },
+    {
+      "taskId": "T-002",
+      "summary": "서브계정 및 권한 관리 기능 개발",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 7,
+      "createdAt": "2025-07-17T02:02:42.157Z",
+      "updatedAt": "2025-07-17T02:02:42.157Z"
+    },
+    {
+      "taskId": "T-003",
+      "summary": "파트너 매칭 시스템 구축",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 7,
+      "urgency": 8,
+      "createdAt": "2025-07-17T02:02:42.157Z",
+      "updatedAt": "2025-07-17T02:02:42.157Z"
+    },
+    {
+      "taskId": "T-004",
+      "summary": "대시보드(매출·주문·정산) 개발",
+      "status": "DONE",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 6,
+      "createdAt": "2025-07-17T02:02:42.157Z",
+      "updatedAt": "2025-07-21T06:34:50.862Z"
+    },
+    {
+      "taskId": "T-005",
+      "summary": "벤더사 대시보드 페이지 기본 구조 설계 및 라우팅",
+      "status": "DONE",
+      "importance": "MUST",
+      "complexity": 5,
+      "urgency": 8,
+      "createdAt": "2025-07-17T02:18:17.743Z",
+      "updatedAt": "2025-07-21T06:40:17.764Z"
+    },
+    {
+      "taskId": "T-006",
+      "summary": "주문 데이터 API 연동 및 상태별 분류 로직 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 8,
+      "createdAt": "2025-07-17T02:18:17.743Z",
+      "updatedAt": "2025-07-17T02:18:17.743Z"
+    },
+    {
+      "taskId": "T-007",
+      "summary": "공통 그리드 컴포넌트 활용 리스트 뷰 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 7,
+      "createdAt": "2025-07-17T02:18:17.743Z",
+      "updatedAt": "2025-07-17T02:18:17.743Z"
+    },
+    {
+      "taskId": "T-008",
+      "summary": "대시보드 요약 정보 위젯/카드 구현",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 4,
+      "urgency": 6,
+      "createdAt": "2025-07-17T02:18:17.743Z",
+      "updatedAt": "2025-07-17T02:18:17.743Z"
+    },
+    {
+      "taskId": "T-009",
+      "summary": "반응형 UI 및 접근성 검증",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 4,
+      "urgency": 5,
+      "createdAt": "2025-07-17T02:18:17.743Z",
+      "updatedAt": "2025-07-17T02:18:17.743Z"
+    },
+    {
+      "taskId": "T-010",
+      "summary": "제품 등록 기능 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 8,
+      "createdAt": "2025-07-17T07:44:43.699Z",
+      "updatedAt": "2025-07-17T07:44:43.699Z"
+    },
+    {
+      "taskId": "T-011",
+      "summary": "제품 수정 및 소프트 삭제 기능 구현",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 6,
+      "urgency": 7,
+      "createdAt": "2025-07-17T07:44:43.699Z",
+      "updatedAt": "2025-07-17T07:44:43.699Z"
+    },
+    {
+      "taskId": "T-012",
+      "summary": "제품 상태·노출 변경 및 인플루언서 노출 제어",
+      "status": "BACKLOG",
+      "importance": "MUST",
+      "complexity": 5,
+      "urgency": 8,
+      "createdAt": "2025-07-17T07:44:43.699Z",
+      "updatedAt": "2025-07-17T07:44:43.699Z"
+    },
+    {
+      "taskId": "T-013",
+      "summary": "제품 변경 이력 기록 기능 구현",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 4,
+      "urgency": 5,
+      "createdAt": "2025-07-17T07:44:43.699Z",
+      "updatedAt": "2025-07-17T07:44:43.699Z"
+    },
+    {
+      "taskId": "T-014",
+      "summary": "상태·노출 변경 알림 기능 연동",
+      "status": "BACKLOG",
+      "importance": "SHOULD",
+      "complexity": 5,
+      "urgency": 6,
+      "createdAt": "2025-07-17T07:44:43.699Z",
+      "updatedAt": "2025-07-17T07:44:43.699Z"
+    },
+    {
+      "taskId": "T-015",
+      "summary": "벤더사 검색 및 탐색 기능 구현",
+      "status": "IN_PROGRESS",
+      "importance": "MUST",
+      "complexity": 5,
+      "urgency": 8,
+      "createdAt": "2025-07-21T06:24:11.558Z",
+      "updatedAt": "2025-07-21T06:41:11.982Z"
+    }
+  ]
+}

+ 36 - 0
.vooster/tasks/T-001.txt

@@ -0,0 +1,36 @@
+# 공통 인증 및 권한 관리 모듈 설계 및 구현
+
+**Task ID:** T-001
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 5/10
+**Urgency:** 7/10
+**Dependencies:** None
+
+## Description
+
+# 설명
+프로젝트 전반에서 재사용될 JWT 인증(JWT) 및 OAuth2 SNS 로그인(Google, Kakao, Naver), 역할기반 권한 관리 미들웨어를 설계하고 구현합니다.
+## 구현 상세
+1. Backend (CodeIgniter4)
+   - `AuthController` 생성 및 JWT 발급/검증 서비스 구현
+   - OAuth2 라이브러리(league/oauth2-client)로 Google/Kakao/Naver 전략 설정
+   - CI4 Migration으로 user, roles, permissions, user_roles, role_permissions 테이블 설계
+2. BFF (Node.js + Express)
+   - 로그인·토큰 갱신 API 작성, Axios 인터셉터로 JWT 검증 미들웨어 연동
+3. Frontend (Nuxt3 + Vue3)
+   - `auth.plugin.ts`로 JWT 토큰 관리 플러그인 작성
+   - Axios 요청/응답 인터셉터로 토큰 자동 갱신 처리
+   - Pinia auth store 구현, `middleware/auth.global.js`에서 경로별 권한 체크 로직 추가
+4. CI/CD 및 배포
+   - GitHub Actions에 lint, test, build, deploy 파이프라인 구성
+   - Dockerfile, Kubernetes Deployment/Service 정의
+## 테스트 전략
+- Backend 유닛 테스트: JWT 발급·검증 성공/실패 케이스 커버리지 90% 이상
+- BFF 통합 테스트: 로그인->토큰 갱신->인증 미들웨어 흐름 E2E 검증
+- Frontend E2E(Cypress): SNS 로그인 버튼 클릭, 리디렉션, 토큰 저장 및 보호된 라우트 접근 제어
+
+---
+
+**Created:** 2025-07-17T02:02:42.157Z
+**Updated:** 2025-07-17T02:02:42.157Z

+ 38 - 0
.vooster/tasks/T-002.txt

@@ -0,0 +1,38 @@
+# 서브계정 및 권한 관리 기능 개발
+
+**Task ID:** T-002
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 7/10
+**Dependencies:** T-001
+
+## Description
+
+# 설명
+벤더사 마스터 계정 아래 팀원용 서브계정을 생성·초대·삭제하고 업무별 읽기/쓰기/관리자 권한을 설정해 UI와 API 접근을 제어합니다.
+## 구현 상세
+1. DB 설계 및 Migration(CI4)
+   - `sub_accounts`, `sub_account_invites`, `sub_account_roles` 테이블 추가
+   - 외래키, 인덱스 구성 및 Redis 캐시 전략 설계
+2. Backend API(CI4)
+   - SubAccountController에 CRUD, 초대 발송, 초대 수락/거절 메서드 구현
+   - RoleController로 역할 생성/조회/수정/삭제 기능 구현
+   - 미들웨어에서 권한 체크 로직 추가 (JWT 기반, role_permissions 활용)
+3. BFF (Node.js)
+   - 프론트엔드 전용 엔드포인트 정의 및 Node-level 권한 유효성 검사 추가
+4. Frontend(Nuxt3 + Vuetify)
+   - 서브계정 관리 UI 페이지 및 모달 컴포넌트 개발(excelUpload, confirmDialog 활용)
+   - 초대 이메일 발송 로직, 역할별 UI 컴포넌트 활성화/비활성화 구현
+   - Pinia store로 서브계정 상태 관리, middleware/auth.global.js에 권한 분기 추가
+5. 알림 연동(WebSocket)
+   - Socket.IO로 초대·역할 변경 알림 실시간 수신 처리
+## 테스트 전략
+- Backend 단위 테스트: API 엔드포인트 입출력, 권한 미들링 성공/실패 케이스 검증
+- Frontend 유닛 테스트: 컴포넌트 렌더링, 버튼 활성화/비활성화 로직 커버리지 80% 이상
+- E2E(Cypress): 서브계정 초대 이메일 링크 클릭, 계정 생성 후 UI 기능 접근 제어 검증
+
+---
+
+**Created:** 2025-07-17T02:02:42.157Z
+**Updated:** 2025-07-17T02:02:42.157Z

+ 42 - 0
.vooster/tasks/T-003.txt

@@ -0,0 +1,42 @@
+# 파트너 매칭 시스템 구축
+
+**Task ID:** T-003
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 7/10
+**Urgency:** 8/10
+**Dependencies:** T-001, T-002
+
+## Description
+
+# 설명
+벤더사가 설정한 카테고리·팔로워 수 등 조건에 따라 인플루언서를 추천하고, 제안·수락·거절 플로우와 알림을 구현합니다.
+## 구현 상세
+1. DB 설계 및 Migration(CI4)
+   - `matching_requests`, `matching_rules`, `influencer_profiles` 테이블 추가
+   - 주요 조회 컬럼 인덱스 구축 및 Redis 캐싱 전략 수립
+2. 백엔드 매칭 알고리즘(CI4 Service)
+   - 조건 필터링 로직 구현(PHP pseudo-code):
+     ```php
+     $query = $this->influencerModel->whereIn('category', $rules->categories)
+                                     ->where('followers >=', $rules->min_followers)
+                                     ->limit(50)
+                                     ->findAll();
+     ```
+   - BFF(Node.js)로 프론트엔드 전용 `GET /matches` API 작성
+3. Frontend(Nuxt3)
+   - 검색 UI 및 추천 리스트 컴포넌트 구현, Vuetify DataTable로 페이징/필터링 처리
+   - 제안·수락·거절 버튼 클릭 시 Socket.IO를 통해 실시간 알림 발송/수신 처리
+4. 알림 센터 연동
+   - 주문/승인 알림과 동일한 WebSocket 채널 사용, Subscription 이벤트 등록
+5. 확장성 고려
+   - 향후 AI 추천 모듈 연동을 위한 추상화 인터페이스 정의
+## 테스트 전략
+- 매칭 알고리즘 유닛 테스트: 다양한 조건 조합 테스트 커버리지 90% 이상
+- API 통합 테스트: `GET /matches`, `POST /requests`, `PATCH /requests/:id` 흐름 검증
+- Frontend E2E: 검색 필터, 제안·수락·거절 플로우, 실시간 알림 수신 검증
+
+---
+
+**Created:** 2025-07-17T02:02:42.157Z
+**Updated:** 2025-07-17T02:02:42.157Z

+ 35 - 0
.vooster/tasks/T-004.txt

@@ -0,0 +1,35 @@
+# 대시보드(매출·주문·정산) 개발
+
+**Task ID:** T-004
+**Status:** DONE
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 6/10
+**Dependencies:** T-001, T-002
+
+## Description
+
+# 설명
+벤더사 및 인플루언서 거래 현황(매출, 주문 수, 정산 완료율)을 집계하고, 차트·기간별 필터링 UI로 시각화합니다.
+## 구현 상세
+1. DB 집계 쿼리(CI4)
+   - 월별/일별 매출, 주문 수, 정산 완료율 계산용 뷰 또는 Stored Procedure 작성
+   - Redis 캐싱으로 조회 성능 최적화
+2. Backend API(CI4)
+   - `DashboardController`에서 `getMetrics(filterParams)` 메서드 구현
+   - BFF(Node.js)로 `GET /dashboard/metrics` 엔드포인트 작성, 캐시 미들기 및 파라미터 검증 추가
+3. Frontend(Nuxt3 + Chart.js 또는 Vuetify Chart)
+   - 대시보드 페이지 컴포넌트 개발, Chart.js 플러그인(useChart composable) 활용
+   - 기간/파트너별 드롭다운 필터링, 로딩 상태, 에러 핸들링 UI 구현
+   - 코드 스플리팅(dynamically import)으로 초기 로드 최적화
+4. 반응형 디자인 및 접근성(WCAG 2.1 AA) 적용
+## 테스트 전략
+- Backend 단위 테스트: 집계 로직 정확성, 캐시 사용 검증
+- API 부하 테스트: 평균 응답 300ms 이하, 동시 1,000 요청 상황에서 95 Percentile 만족 여부 측정
+- Frontend Snapshot 테스트: 차트 렌더링, 필터링 동작 검증
+- E2E(Cypress): 대시보드 필터링, 데이터 일관성, 반응형 레이아웃 검증
+
+---
+
+**Created:** 2025-07-17T02:02:42.157Z
+**Updated:** 2025-07-21T06:34:50.862Z

+ 25 - 0
.vooster/tasks/T-005.txt

@@ -0,0 +1,25 @@
+# 벤더사 대시보드 페이지 기본 구조 설계 및 라우팅
+
+**Task ID:** T-005
+**Status:** DONE
+**Importance:** MUST
+**Complexity:** 5/10
+**Urgency:** 8/10
+**Dependencies:** None
+
+## Description
+
+# 설명
+벤더사 전용 메인 대시보드 페이지의 기본 레이아웃과 로그인 후 라우팅을 설정합니다.
+# 구현 세부사항
+1. Nuxt3 라우터 파일에 `'/vendor/dashboard'` 경로 추가
+2. 로그인 완료 후 `router.push('/vendor/dashboard')` 호출 로직 구현
+3. v-header, v-menu, Container 컴포넌트로 레이아웃 구조 정의
+# 테스트 전략
+- 인증되지 않은 상태에서 `/vendor/dashboard` 접근 시 로그인 페이지로 리다이렉트 확인
+- 로그인 후 자동 진입 및 레이아웃 요소 렌더링 검증
+
+---
+
+**Created:** 2025-07-17T02:18:17.743Z
+**Updated:** 2025-07-21T06:40:17.764Z

+ 27 - 0
.vooster/tasks/T-006.txt

@@ -0,0 +1,27 @@
+# 주문 데이터 API 연동 및 상태별 분류 로직 구현
+
+**Task ID:** T-006
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 8/10
+**Dependencies:** T-005
+
+## Description
+
+# 설명
+백엔드에서 주문 데이터를 받아와 상태별로 분류 및 전처리 로직을 구현합니다.
+# 구현 세부사항
+1. Axios로 주문 데이터 API(`GET /api/orders`) 호출
+2. 응답 데이터에서 상태값(`status`) 기준으로 신규주문, 배송중, 배송완료 배열 분리
+3. 각 배열별 필요한 필드만 매핑(id, influencerName, productName, amount, contact, email, status, requestDate)
+4. Vuex 또는 Pinia 스토어에 상태별 배열 저장
+# 테스트 전략
+- Mock API 응답으로 상태별 배열 분리 로직 단위 테스트
+- 잘못된 상태값 처리 예외 케이스 테스트
+- 스토어에 저장된 데이터 구조 및 값 검증
+
+---
+
+**Created:** 2025-07-17T02:18:17.743Z
+**Updated:** 2025-07-17T02:18:17.743Z

+ 30 - 0
.vooster/tasks/T-007.txt

@@ -0,0 +1,30 @@
+# 공통 그리드 컴포넌트 활용 리스트 뷰 구현
+
+**Task ID:** T-007
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 7/10
+**Dependencies:** T-006
+
+## Description
+
+# 설명
+공통 그리드 컴포넌트를 활용해 상태별 주문 리스트를 구현합니다.
+# 구현 세부사항
+1. 공통 DataGrid 컴포넌트 import 및 props 바인딩
+2. 신규주문, 배송중, 배송완료 탭 또는 섹션별로 DataGrid 호출
+3. 컬럼 정의
+   - 신규주문: `influencerName`, `productName`, `amount`, `contact`, `email`, `status`, `requestDate`
+   - 배송중: `influencerName`, `item`, `manager`, `contact`, `email`, `status`, `requestDate`
+   - 배송완료: 캡틴과 협의한 컬럼 설정
+4. 스토어에서 상태별 데이터 전달 및 로딩/오류 처리
+# 테스트 전략
+- 각각의 그리드가 올바른 컬럼으로 렌더링되는지 테스트
+- 데이터 바인딩 후 행(row) 개수 및 값 검증
+- 로딩 상태 및 에러 메시지 표시 테스트
+
+---
+
+**Created:** 2025-07-17T02:18:17.743Z
+**Updated:** 2025-07-17T02:18:17.743Z

+ 27 - 0
.vooster/tasks/T-008.txt

@@ -0,0 +1,27 @@
+# 대시보드 요약 정보 위젯/카드 구현
+
+**Task ID:** T-008
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 4/10
+**Urgency:** 6/10
+**Dependencies:** T-007
+
+## Description
+
+# 설명
+대시보드 상단에 상태별 주문 건수를 표시하는 요약 위젯을 구현합니다.
+# 구현 세부사항
+1. Vue 컴포넌트(`SummaryCard`) 생성
+2. 스토어 또는 API에서 상태별 건수 계산(fetchSummaryCount API 또는 캐시 활용)
+3. 카드 레이아웃에 아이콘, 레이블, 건수 표시
+4. 클릭 시 해당 리스트 섹션으로 스크롤 또는 필터 토글 구현
+# 테스트 전략
+- 상태별 건수가 올바르게 계산되어 표시되는지 확인
+- 카드 클릭 시 스크롤/필터링 기능 동작 검증
+- 반응형에서 카드 레이아웃 이상 유무 테스트
+
+---
+
+**Created:** 2025-07-17T02:18:17.743Z
+**Updated:** 2025-07-17T02:18:17.743Z

+ 26 - 0
.vooster/tasks/T-009.txt

@@ -0,0 +1,26 @@
+# 반응형 UI 및 접근성 검증
+
+**Task ID:** T-009
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 4/10
+**Urgency:** 5/10
+**Dependencies:** T-007, T-008
+
+## Description
+
+# 설명
+대시보드의 반응형 디자인 및 접근성(WCAG 2.1 AA) 준수를 검증 및 개선합니다.
+# 구현 세부사항
+1. CSS 미디어 쿼리로 데스크탑, 태블릿, 모바일 레이아웃 조정
+2. Vuetify 또는 custom style로 접근성 속성(aria-label, tabindex) 추가
+3. 키보드 네비게이션 및 화면 리더 검증
+# 테스트 전략
+- Chrome DevTools 디바이스 모드에서 각 화면 크기 테스트
+- axe-core 또는 Lighthouse 접근성 자동 검증
+- 키보드만으로 네비게이션 테스트 및 스크린리더 읽기 확인
+
+---
+
+**Created:** 2025-07-17T02:18:17.743Z
+**Updated:** 2025-07-17T02:18:17.743Z

+ 28 - 0
.vooster/tasks/T-010.txt

@@ -0,0 +1,28 @@
+# 제품 등록 기능 구현
+
+**Task ID:** T-010
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 8/10
+**Dependencies:** T-001
+
+## Description
+
+### 설명
+벤더사 전용 제품 등록 UI와 API를 구현합니다.
+### 구현 상세
+1. Authorization 미들웨어(T-001) 적용 및 JWT role 검증
+2. 프론트엔드(Vue3/Nuxt3) 등록 폼 컴포넌트 작성(제품명, 공급가, 판매가, 배송비, 소타이틀, 상세내용, 파일첨부, 상태, 노출상태, 업데이트 내역 필드)
+3. 파일 업로드 기능 구현(Axios + FormData, 확장자/용량 제한)
+4. 백엔드(CodeIgniter4) 컨트롤러 및 모델 생성 및 라우팅 설정(`POST /api/products`)
+5. DB 저장 로직 작성(MySQL(RDS) products 테이블, 업로드 파일 메타정보 저장)
+### 테스트 전략
+- 유닛 테스트: 입력 필드 유효성 검증 로직 테스트
+- 통합 테스트: API 요청 시 정상 저장 및 에러 응답 테스트
+- E2E 테스트: 실제 파일 업로드 포함된 등록 흐름 테스트
+
+---
+
+**Created:** 2025-07-17T07:44:43.699Z
+**Updated:** 2025-07-17T07:44:43.699Z

+ 28 - 0
.vooster/tasks/T-011.txt

@@ -0,0 +1,28 @@
+# 제품 수정 및 소프트 삭제 기능 구현
+
+**Task ID:** T-011
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 6/10
+**Urgency:** 7/10
+**Dependencies:** T-010
+
+## Description
+
+### 설명
+벤더 전용 제품 수정 및 소프트 삭제 기능을 구현합니다.
+### 구현 상세
+1. 기존 제품 정보 조회 API(`GET /api/products/{id}`) 및 모델 fetch 로직 작성
+2. 수정 폼 컴포넌트 작성 및 기존값 바인딩
+3. 수정 API 구현(`PUT /api/products/{id}`), 업데이트 내역 필수 입력 로직 적용
+4. 파일 재첨부/삭제 처리 로직 구현
+5. 소프트 삭제 API 구현(`DELETE /api/products/{id}`), 노출상태를 비노출로 전환
+### 테스트 전략
+- 유닛 테스트: 필드별 검증 및 업데이트 내역 필수 체크 테스트
+- 통합 테스트: 수정 및 삭제 API 정상 동작 테스트
+- E2E 테스트: UI상에서 수정/삭제 플로우 시나리오 검증
+
+---
+
+**Created:** 2025-07-17T07:44:43.699Z
+**Updated:** 2025-07-17T07:44:43.699Z

+ 27 - 0
.vooster/tasks/T-012.txt

@@ -0,0 +1,27 @@
+# 제품 상태·노출 변경 및 인플루언서 노출 제어
+
+**Task ID:** T-012
+**Status:** BACKLOG
+**Importance:** MUST
+**Complexity:** 5/10
+**Urgency:** 8/10
+**Dependencies:** T-011
+
+## Description
+
+### 설명
+제품의 상태(판매중/품절) 및 노출(노출/비노출) 변경과 인플루언서 포털에서의 노출 제어를 구현합니다.
+### 구현 상세
+1. 상태·노출 변경 UI(select) 컴포넌트 작성
+2. API 구현(`PATCH /api/products/{id}/status`), 노출 변경 로직 반영
+3. 인플루언서 포털 API 조회 시 비노출 상품 필터링(BFF Node.js, MySQL)
+4. 상태 변경 후 UI 실시간 리로드/갱신 처리
+### 테스트 전략
+- 단위 테스트: 상태 변경 로직 및 필터링 로직 검증
+- 통합 테스트: API 호출 후 상태·노출 반영 및 필터링 확인
+- E2E 테스트: 인플루언서 포털 상품 리스트에 비노출 상품 미노출 확인
+
+---
+
+**Created:** 2025-07-17T07:44:43.699Z
+**Updated:** 2025-07-17T07:44:43.699Z

+ 27 - 0
.vooster/tasks/T-013.txt

@@ -0,0 +1,27 @@
+# 제품 변경 이력 기록 기능 구현
+
+**Task ID:** T-013
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 4/10
+**Urgency:** 5/10
+**Dependencies:** T-011
+
+## Description
+
+### 설명
+제품 정보 수정 및 상태·노출 변경 시 변경 이력을 기록하는 기능을 구현합니다.
+### 구현 상세
+1. 변경 이력 테이블(product_history) 마이그레이션 스크립트 작성
+2. 수정/상태 변경 API 후킹 지점에 이력 저장 로직 삽입(Who, When, What)
+3. 서비스 계층에 이력 저장 메서드 구현
+4. 이력 조회 UI/이력 리스트 컴포넌트 목업 작성
+### 테스트 전략
+- 유닛 테스트: 이력 저장 메서드 동작 테스트
+- 통합 테스트: 수정 API 호출 시 history 레코드 생성 확인
+- DB 테스트: 마이그레이션 및 데이터 적재 확인
+
+---
+
+**Created:** 2025-07-17T07:44:43.699Z
+**Updated:** 2025-07-17T07:44:43.699Z

+ 27 - 0
.vooster/tasks/T-014.txt

@@ -0,0 +1,27 @@
+# 상태·노출 변경 알림 기능 연동
+
+**Task ID:** T-014
+**Status:** BACKLOG
+**Importance:** SHOULD
+**Complexity:** 5/10
+**Urgency:** 6/10
+**Dependencies:** T-012
+
+## Description
+
+### 설명
+제품 상태 또는 노출 변경 시 관계자에게 웹/이메일/실시간 알림을 전송하는 기능을 연동합니다.
+### 구현 상세
+1. 알림센터 연동 모듈 구성(웹소켓, 이메일 서비스, 내부 알림센터 API)
+2. 상태·노출 변경 이벤트 발생 시 알림 트리거 로직 작성(구독자 분기: 벤더, 인플루언서)
+3. 알림 API 호출 또는 웹소켓 메시지 전송 구현
+4. 프론트엔드 알림 컴포넌트와 연동해 실시간 표시 처리
+### 테스트 전략
+- 단위 테스트: 이벤트 핸들러 및 알림 모듈 동작 검증
+- 통합 테스트: 알림센터/이메일 모의(Mock) 연동 테스트
+- E2E 테스트: 상태 변경 시 웹소켓 알림 표시 확인
+
+---
+
+**Created:** 2025-07-17T07:44:43.699Z
+**Updated:** 2025-07-17T07:44:43.699Z

+ 30 - 0
.vooster/tasks/T-015.txt

@@ -0,0 +1,30 @@
+# 벤더사 검색 및 탐색 기능 구현
+
+**Task ID:** T-015
+**Status:** IN_PROGRESS
+**Importance:** MUST
+**Complexity:** 5/10
+**Urgency:** 8/10
+**Dependencies:** None
+
+## Description
+
+#### 설명
+인플루언서가 벤더사를 조건(이름, 카테고리 등)으로 검색하고, 프로필/정보를 확인할 수 있는 리스트 뷰 및 검색 필터 기능을 구현합니다.
+
+#### 구현 세부사항
+1. API 설계 및 연동: GET /vendors?name=&category=&page=&size= 엔드포인트 구현 및 Axios 연동
+2. 검색/필터 UI: Vue3 Composition API와 Vuetify의 v-text-field, v-select, v-data-table 컴포넌트 사용하여 조건 입력 및 결과 리스트 렌더링 
+3. 페이징: 서버 응답에서 totalCount, currentPage, pageSize 반환 후 UI에 v-pagination 적용
+4. 상세 조회: 리스트 아이템 클릭 시 라우터 네비게이션으로 /vendors/:id 페이지 이동, GET /vendors/{id} 호출하여 데이터 표시
+5. 상태 관리: Pinia store modules.vendors에 검색조건, 결과, 로딩/에러 상태 관리
+
+#### 테스트 전략
+- 단위 테스트: Vitest와 Axios mock adapter를 활용하여 API 호출 및 상태 관리 로직 검증
+- E2E 테스트: Cypress로 검색 필드에 조건 입력 후 결과 리스트 및 페이징 동작 검증
+- UI 테스트: Vuetify 컴포넌트 렌더링 및 사용자 인터랙션(검색, 페이지 이동) 테스트
+
+---
+
+**Created:** 2025-07-21T06:24:11.558Z
+**Updated:** 2025-07-21T06:41:11.982Z

+ 43 - 0
assets/scss/main.scss

@@ -580,4 +580,47 @@ html {
       transition: all 0.7s cubic-bezier(0.25, 0.8, 0.25, 1);
     }
   }
+}
+
+
+.order--quick--menu{
+  padding-top:25px;
+  .order--box{
+    >ul{
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      gap:20px;
+      width:100%;
+      overflow-x: auto;
+      >li{
+        border:1px solid #ddd;
+        border-radius: 15px;
+        min-width:150px;
+        padding:20px;
+        h2{
+          font-size:18px;
+          font-weight: 900;
+        }
+        .item--count{
+          padding-top:15px;
+          display: flex;
+          align-items: center;
+          justify-content: flex-start;
+          gap:20px;
+          font-size:25px;
+          font-weight: 900;
+          i{
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            width:80px;
+            height:80px;
+            border-radius: 80px;
+            background-color: #9475EC;
+          }
+        }
+      }
+    }
+  }
 }

+ 16 - 0
composables/useApi.js

@@ -3,6 +3,22 @@
 const API_ENDPOINTS = {
   //뉴스룸
   newsInsert: ``,
+  
+  // 벤더사 관련 API
+  vendors: {
+    // 벤더사 검색 및 목록 조회
+    search: '/vendors',
+    // 벤더사 상세 조회
+    detail: '/vendors/:id',
+    // 벤더사 등록
+    create: '/vendors',
+    // 벤더사 수정
+    update: '/vendors/:id',
+    // 벤더사 삭제
+    delete: '/vendors/:id',
+    // 벤더사 카테고리 목록
+    categories: '/vendors/categories',
+  },
 };
 
 export default API_ENDPOINTS;

+ 2 - 1
middleware/auth.global.js

@@ -6,7 +6,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
   const tokenPassPages = [
     '/', 
     '/roulette',    
-    '/auth',    
+    '/auth',
+    '/vendor'
   ]
   //let accountValue = useAuthStore().getAccountRole.charAt(0).toUpperCase()
 

+ 105 - 2
pages/auth/join.vue

@@ -32,7 +32,11 @@
               v-model="form.formValue1"
               placeholder="아이디를 입력해주세요"
               class="custom-input"
+              @blur="checkId"
             ></v-text-field>
+            <v-btn v-if="!useStore.getSnsTempData?.ID" small class="ml-2" @click="checkId"
+              >중복확인</v-btn
+            >
           </div>
 
           <div class="txt-field-box">
@@ -159,6 +163,13 @@
               placeholder="아이디를 입력해주세요"
               class="custom-input"
             ></v-text-field>
+            <v-btn
+              v-if="!useStore.getSnsTempData?.ID"
+              small
+              class="ml-2"
+              @click="checkIdVendor"
+              >중복확인</v-btn
+            >
           </div>
 
           <div class="txt-field-box">
@@ -290,6 +301,7 @@
    ************************/
   import apiUrl from "@/composables/useApi";
   import { useI18n } from "vue-i18n";
+  const { $log, $toast } = useNuxtApp();
 
   /************************
    *    layout setting
@@ -422,7 +434,92 @@
     return result;
   };
 
-  const joinMember = (id_type) => {
+  const checkId = async () => {
+    if (!form.value.formValue1) {
+      $toast.error("아이디를 입력해주세요.");
+      return;
+    }
+
+    // 아이디 형식 검사 (영문, 숫자 조합 6~20자)
+    const idRegex = /^[a-zA-Z0-9]{6,20}$/;
+    if (!idRegex.test(form.value.formValue1)) {
+      $toast.error("아이디는 영문, 숫자 조합 6~20자로 입력해주세요.");
+      return;
+    }
+
+    try {
+      const response = await useAxios().post("/auth/checkId", {
+        ID: form.value.formValue1,
+        TYPE: "influence",
+      });
+
+      if (response.data.isDuplicate) {
+        $toast.error("이미 사용중인 아이디입니다.");
+      } else {
+        $toast.success("사용 가능한 아이디입니다.");
+      }
+    } catch (error) {
+      console.error("ID check error:", error);
+      $toast.error("아이디 중복 확인 중 오류가 발생했습니다.");
+    }
+  };
+
+  const checkIdVendor = async () => {
+    if (!formVendor.value.formValue1) {
+      $toast.error("아이디를 입력해주세요.");
+      return;
+    }
+
+    // 아이디 형식 검사 (영문, 숫자 조합 6~20자)
+    const idRegex = /^[a-zA-Z0-9]{6,20}$/;
+    if (!idRegex.test(formVendor.value.formValue1)) {
+      $toast.error("아이디는 영문, 숫자 조합 6~20자로 입력해주세요.");
+      return;
+    }
+
+    try {
+      const response = await useAxios().post("/auth/checkId", {
+        ID: formVendor.value.formValue1,
+        TYPE: "vendor",
+      });
+
+      if (response.data.isDuplicate) {
+        $toast.error("이미 사용중인 아이디입니다.");
+      } else {
+        $toast.success("사용 가능한 아이디입니다.");
+      }
+    } catch (error) {
+      console.error("ID check error:", error);
+      $toast.error("아이디 중복 확인 중 오류가 발생했습니다.");
+    }
+  };
+  // 회원가입 전에 아이디 유효성 검사 추가
+  const joinMember = async (id_type) => {
+    if (!useStore.getSnsTempData?.ID) {
+      if (id_type === "vendor") {
+        if (!formVendor.value.formValue1) {
+          $toast.error("아이디를 입력해주세요.");
+          return;
+        }
+        // const idRegex = /^[a-zA-Z0-9]{6,20}$/;
+        // if (!idRegex.test(form.value.formValue1)) {
+        //   $toast.error("아이디는 영문, 숫자 조합 6~20자로 입력해주세요.");
+        //   return;
+        // }
+      } else {
+        if (!formVendor.value.formValue1) {
+          $toast.error("아이디를 입력해주세요.");
+          return;
+        }
+
+        const idRegex = /^[a-zA-Z0-9]{6,20}$/;
+        if (!idRegex.test(form.value.formValue1)) {
+          $toast.error("아이디는 영문, 숫자 조합 6~20자로 입력해주세요.");
+          return;
+        }
+      }
+    }
+
     let _req = "";
     let _api = "";
     if (id_type === "influence") {
@@ -459,8 +556,14 @@
         if (_req.TYPE === "1") {
           // SNS 가입일 경우
           useStore.setTempData("");
+          useUtil.setPageMove("/?type=influence");
+          return;
+        }
+        if (form.value.formValue0 === "Y") {
+          useUtil.setPageMove("/?type=influence");
+        } else {
+          useUtil.setPageMove("/?type=vendor");
         }
-        //useUtil.setPageMove("/");
       })
       .catch((error) => {
         if (error.response) {

+ 27 - 0
pages/index.vue

@@ -699,4 +699,31 @@
     //     $log.debug("[login][fnLogin][finished]");
     //   });
   }
+
+  /**
+   * @SCRIPT
+   * 비밀번호확인 validation
+   */
+  function fnValidCheck(type) {
+    // 아이디 체크
+    if (type === "id") {
+      const id = loginForm.value.userId;
+
+      // 1. 기본 유효성 검사
+      if (!id) {
+        $toast.error("아이디를 입력해주세요.");
+        return false;
+      }
+
+      // 2. 아이디 형식 검사 (영문, 숫자 조합 6~20자)
+      const idRegex = /^[a-zA-Z0-9]{6,20}$/;
+      if (!idRegex.test(id)) {
+        $toast.error("아이디는 영문, 숫자 조합 6~20자로 입력해주세요.");
+        return false;
+      }
+    }
+
+    // 다른 유효성 검사가 필요한 경우 여기에 추가
+    return true;
+  }
 </script>

+ 609 - 0
pages/view/vendor/[id].vue

@@ -0,0 +1,609 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span @click="goBack" class="breadcrumb-link">벤더사 관리</span>
+        <span>{{ currentVendor?.name || '벤더사 상세' }}</span>
+      </div>
+    </div>
+
+    <!-- 로딩 상태 -->
+    <div v-if="vendorsStore.getLoading.value" class="loading-wrap">
+      <v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
+      <p>벤더사 정보를 불러오고 있습니다...</p>
+    </div>
+
+    <!-- 에러 상태 -->
+    <div v-else-if="vendorsStore.getError.value" class="error-wrap">
+      <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
+        {{ vendorsStore.getError.value }}
+      </v-alert>
+      <v-btn @click="goBack" class="custom-btn btn-blue">목록으로 돌아가기</v-btn>
+    </div>
+
+    <!-- 벤더사 상세 정보 -->
+    <div v-else-if="currentVendor" class="vendor-detail-wrap">
+      <!-- 벤더사 기본 정보 -->
+      <v-card class="vendor-header-card" elevation="2">
+        <v-card-text>
+          <div class="vendor-header">
+            <div class="vendor-logo-section">
+              <v-avatar size="80" class="vendor-logo-large">
+                <v-img
+                  v-if="currentVendor.logo"
+                  :src="currentVendor.logo"
+                  :alt="currentVendor.name + ' 로고'"
+                ></v-img>
+                <div v-else class="no-logo-large">{{ currentVendor.name.charAt(0) }}</div>
+              </v-avatar>
+            </div>
+            <div class="vendor-info-section">
+              <h1 class="vendor-name">{{ currentVendor.name }}</h1>
+              <div class="vendor-meta">
+                <v-chip
+                  :color="getCategoryColor(currentVendor.category)"
+                  size="large"
+                  variant="outlined"
+                  class="mr-2"
+                >
+                  {{ getCategoryName(currentVendor.category) }}
+                </v-chip>
+                <v-chip
+                  :color="currentVendor.status === 'ACTIVE' ? 'success' : 'error'"
+                  size="large"
+                >
+                  {{ currentVendor.status === 'ACTIVE' ? '활성' : '비활성' }}
+                </v-chip>
+              </div>
+              <p v-if="currentVendor.description" class="vendor-description">
+                {{ currentVendor.description }}
+              </p>
+            </div>
+            <div class="vendor-actions">
+              <v-btn
+                v-if="currentVendor.website"
+                :href="currentVendor.website"
+                target="_blank"
+                class="custom-btn btn-white mr-2"
+                prepend-icon="mdi-web"
+              >
+                웹사이트
+              </v-btn>
+              <v-btn
+                @click="goBack"
+                class="custom-btn btn-blue"
+                prepend-icon="mdi-arrow-left"
+              >
+                목록으로
+              </v-btn>
+            </div>
+          </div>
+        </v-card-text>
+      </v-card>
+
+      <!-- 상세 정보 탭 -->
+      <v-card class="detail-tabs-card" elevation="2">
+        <v-tabs v-model="activeTab" class="custom-tabs">
+          <v-tab value="info">기업 정보</v-tab>
+          <v-tab value="contact">연락처</v-tab>
+          <v-tab value="products">제품 정보</v-tab>
+          <v-tab value="partnership">파트너십</v-tab>
+        </v-tabs>
+
+        <v-card-text>
+          <v-tabs-window v-model="activeTab">
+            <!-- 기업 정보 탭 -->
+            <v-tabs-window-item value="info">
+              <div class="info-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>사업자등록번호</h3>
+                      <p>{{ currentVendor.businessNumber || '-' }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>설립일</h3>
+                      <p>{{ formatDate(currentVendor.establishedDate) || '-' }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>직원 수</h3>
+                      <p>{{ currentVendor.employeeCount ? currentVendor.employeeCount + '명' : '-' }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>연매출</h3>
+                      <p>{{ formatCurrency(currentVendor.annualRevenue) || '-' }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12">
+                    <div class="info-item">
+                      <h3>사업 분야</h3>
+                      <div class="business-areas">
+                        <v-chip
+                          v-for="area in currentVendor.businessAreas || []"
+                          :key="area"
+                          size="small"
+                          variant="outlined"
+                          class="mr-2 mb-2"
+                        >
+                          {{ area }}
+                        </v-chip>
+                      </div>
+                    </div>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 연락처 탭 -->
+            <v-tabs-window-item value="contact">
+              <div class="contact-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <v-card variant="outlined" class="contact-card">
+                      <v-card-title>
+                        <v-icon class="mr-2">mdi-account</v-icon>
+                        주요 담당자
+                      </v-card-title>
+                      <v-card-text>
+                        <div class="contact-item">
+                          <strong>이름:</strong> {{ currentVendor.contactName || '-' }}
+                        </div>
+                        <div class="contact-item">
+                          <strong>직책:</strong> {{ currentVendor.contactPosition || '-' }}
+                        </div>
+                        <div class="contact-item">
+                          <strong>전화:</strong> 
+                          <a v-if="currentVendor.contactPhone" :href="`tel:${currentVendor.contactPhone}`">
+                            {{ currentVendor.contactPhone }}
+                          </a>
+                          <span v-else>-</span>
+                        </div>
+                        <div class="contact-item">
+                          <strong>이메일:</strong>
+                          <a v-if="currentVendor.contactEmail" :href="`mailto:${currentVendor.contactEmail}`">
+                            {{ currentVendor.contactEmail }}
+                          </a>
+                          <span v-else>-</span>
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <v-card variant="outlined" class="contact-card">
+                      <v-card-title>
+                        <v-icon class="mr-2">mdi-map-marker</v-icon>
+                        주소 정보
+                      </v-card-title>
+                      <v-card-text>
+                        <div class="contact-item">
+                          <strong>주소:</strong>
+                          <p>{{ currentVendor.address || '-' }}</p>
+                        </div>
+                        <div class="contact-item">
+                          <strong>상세주소:</strong>
+                          <p>{{ currentVendor.detailAddress || '-' }}</p>
+                        </div>
+                        <div class="contact-item">
+                          <strong>우편번호:</strong> {{ currentVendor.zipCode || '-' }}
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 제품 정보 탭 -->
+            <v-tabs-window-item value="products">
+              <div class="products-section">
+                <div class="section-header">
+                  <h3>주요 제품/서비스</h3>
+                </div>
+                <v-row v-if="currentVendor.products && currentVendor.products.length > 0">
+                  <v-col
+                    v-for="product in currentVendor.products"
+                    :key="product.id"
+                    cols="12"
+                    md="6"
+                    lg="4"
+                  >
+                    <v-card class="product-card" variant="outlined">
+                      <v-img
+                        v-if="product.image"
+                        :src="product.image"
+                        height="150"
+                        cover
+                      ></v-img>
+                      <v-card-title>{{ product.name }}</v-card-title>
+                      <v-card-text>
+                        <p>{{ product.description }}</p>
+                        <div class="product-price" v-if="product.price">
+                          {{ formatCurrency(product.price) }}
+                        </div>
+                      </v-card-text>
+                    </v-card>
+                  </v-col>
+                </v-row>
+                <div v-else class="no-data">
+                  <v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
+                  <p>등록된 제품 정보가 없습니다</p>
+                </div>
+              </div>
+            </v-tabs-window-item>
+
+            <!-- 파트너십 탭 -->
+            <v-tabs-window-item value="partnership">
+              <div class="partnership-section">
+                <v-row>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>파트너십 등급</h3>
+                      <v-chip
+                        :color="getPartnershipColor(currentVendor.partnershipLevel)"
+                        size="large"
+                      >
+                        {{ getPartnershipName(currentVendor.partnershipLevel) }}
+                      </v-chip>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>협력 시작일</h3>
+                      <p>{{ formatDate(currentVendor.partnershipStartDate) || '-' }}</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>협력 프로젝트 수</h3>
+                      <p>{{ currentVendor.projectCount || 0 }}개</p>
+                    </div>
+                  </v-col>
+                  <v-col cols="12" md="6">
+                    <div class="info-item">
+                      <h3>평점</h3>
+                      <div class="rating">
+                        <v-rating
+                          v-model="currentVendor.rating"
+                          readonly
+                          size="small"
+                          density="compact"
+                        ></v-rating>
+                        <span class="rating-text">{{ currentVendor.rating || 0 }}/5</span>
+                      </div>
+                    </div>
+                  </v-col>
+                  <v-col cols="12">
+                    <div class="info-item">
+                      <h3>특이사항</h3>
+                      <p>{{ currentVendor.notes || '특이사항이 없습니다.' }}</p>
+                    </div>
+                  </v-col>
+                </v-row>
+              </div>
+            </v-tabs-window-item>
+          </v-tabs-window>
+        </v-card-text>
+      </v-card>
+    </div>
+
+    <!-- 데이터가 없을 때 -->
+    <div v-else class="no-data-wrap">
+      <div class="no-data">
+        <v-icon size="64" color="grey-lighten-1">mdi-store-alert</v-icon>
+        <h3>벤더사 정보를 찾을 수 없습니다</h3>
+        <p>요청하신 벤더사가 존재하지 않거나 삭제되었을 수 있습니다</p>
+        <v-btn @click="goBack" class="custom-btn btn-blue">목록으로 돌아가기</v-btn>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { useVendorsStore } from '@/stores/vendors'
+
+/************************************************************************
+|    레이아웃
+************************************************************************/
+definePageMeta({
+  layout: "default",
+})
+
+/************************************************************************
+|    스토어, 라우터, 라우트
+************************************************************************/
+const vendorsStore = useVendorsStore()
+const router = useRouter()
+const route = useRoute()
+
+/************************************************************************
+|    반응형 데이터
+************************************************************************/
+const pageId = ref("벤더사 상세")
+const activeTab = ref("info")
+
+/************************************************************************
+|    computed
+************************************************************************/
+const currentVendor = computed(() => vendorsStore.getCurrentVendor.value)
+
+/************************************************************************
+|    메서드
+************************************************************************/
+const goBack = () => {
+  router.push('/view/vendor/vendors')
+}
+
+const getCategoryColor = (category) => {
+  const colors = {
+    'FASHION_BEAUTY': 'pink',
+    'FOOD_HEALTH': 'green',
+    'LIFESTYLE': 'blue',
+    'TECH_ELECTRONICS': 'purple',
+    'SPORTS_LEISURE': 'orange',
+    'CULTURE_ENTERTAINMENT': 'red'
+  }
+  return colors[category] || 'grey'
+}
+
+const getCategoryName = (category) => {
+  const names = {
+    'FASHION_BEAUTY': '패션·뷰티',
+    'FOOD_HEALTH': '식품·건강',
+    'LIFESTYLE': '라이프스타일',
+    'TECH_ELECTRONICS': '테크·가전',
+    'SPORTS_LEISURE': '스포츠·레저',
+    'CULTURE_ENTERTAINMENT': '문화·엔터테인먼트'
+  }
+  return names[category] || category
+}
+
+const getPartnershipColor = (level) => {
+  const colors = {
+    'PLATINUM': 'purple',
+    'GOLD': 'amber',
+    'SILVER': 'grey',
+    'BRONZE': 'brown',
+    'BASIC': 'blue-grey'
+  }
+  return colors[level] || 'grey'
+}
+
+const getPartnershipName = (level) => {
+  const names = {
+    'PLATINUM': '플래티넘',
+    'GOLD': '골드',
+    'SILVER': '실버',
+    'BRONZE': '브론즈',
+    'BASIC': '베이직'
+  }
+  return names[level] || level
+}
+
+const formatDate = (dateString) => {
+  if (!dateString) return null
+  return new Date(dateString).toLocaleDateString('ko-KR')
+}
+
+const formatCurrency = (amount) => {
+  if (!amount) return null
+  return new Intl.NumberFormat('ko-KR', {
+    style: 'currency',
+    currency: 'KRW'
+  }).format(amount)
+}
+
+/************************************************************************
+|    라이프사이클
+************************************************************************/
+onMounted(async () => {
+  const vendorId = route.params.id
+  if (vendorId) {
+    await vendorsStore.getVendorById(vendorId)
+  }
+})
+</script>
+
+<style scoped>
+.vendor-detail-wrap {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.vendor-header-card {
+  margin-bottom: 20px;
+}
+
+.vendor-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 20px;
+}
+
+.vendor-logo-section {
+  flex-shrink: 0;
+}
+
+.vendor-logo-large {
+  border: 1px solid #e0e0e0;
+}
+
+.no-logo-large {
+  background: #f5f5f5;
+  color: #666;
+  font-weight: bold;
+  font-size: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.vendor-info-section {
+  flex: 1;
+}
+
+.vendor-name {
+  font-size: 28px;
+  font-weight: bold;
+  margin-bottom: 12px;
+}
+
+.vendor-meta {
+  margin-bottom: 16px;
+}
+
+.vendor-description {
+  color: #666;
+  line-height: 1.6;
+  margin: 0;
+}
+
+.vendor-actions {
+  flex-shrink: 0;
+}
+
+.breadcrumb-link {
+  cursor: pointer;
+  color: #1976d2;
+}
+
+.breadcrumb-link:hover {
+  text-decoration: underline;
+}
+
+.detail-tabs-card {
+  margin-top: 20px;
+}
+
+.info-section,
+.contact-section,
+.products-section,
+.partnership-section {
+  padding: 20px 0;
+}
+
+.info-item {
+  margin-bottom: 24px;
+}
+
+.info-item h3 {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 8px;
+  color: #333;
+}
+
+.info-item p {
+  font-size: 14px;
+  color: #666;
+  margin: 0;
+}
+
+.contact-card {
+  height: 100%;
+}
+
+.contact-item {
+  margin-bottom: 12px;
+}
+
+.contact-item strong {
+  display: inline-block;
+  width: 80px;
+  color: #333;
+}
+
+.contact-item a {
+  color: #1976d2;
+  text-decoration: none;
+}
+
+.contact-item a:hover {
+  text-decoration: underline;
+}
+
+.section-header {
+  margin-bottom: 20px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.section-header h3 {
+  font-size: 18px;
+  font-weight: 600;
+  margin: 0;
+}
+
+.product-card {
+  height: 100%;
+}
+
+.product-price {
+  font-weight: bold;
+  color: #1976d2;
+  margin-top: 8px;
+}
+
+.rating {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.rating-text {
+  font-size: 14px;
+  color: #666;
+}
+
+.business-areas {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.loading-wrap,
+.error-wrap,
+.no-data-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+}
+
+.no-data {
+  text-align: center;
+}
+
+.no-data h3 {
+  margin: 16px 0 8px;
+  color: #666;
+}
+
+.no-data p {
+  color: #999;
+  margin-bottom: 20px;
+}
+
+@media (max-width: 768px) {
+  .vendor-header {
+    flex-direction: column;
+  }
+  
+  .vendor-actions {
+    width: 100%;
+  }
+}
+</style>

+ 768 - 0
pages/view/vendor/dashboard/index.vue

@@ -0,0 +1,768 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <!-- 필터링 섹션 -->
+    <div class="dashboard--filters">
+      <div class="filter--wrap">
+        <div class="date--range">
+          <VueDatePicker
+            v-model="dateRange"
+            range
+            :format="dateFormat"
+            placeholder="기간을 선택하세요"
+            :auto-apply="true"
+            @update:model-value="onDateRangeChange"
+          />
+        </div>
+        <div class="quick--filters">
+          <v-btn 
+            :class="{ 'active': selectedPeriod === 'today' }"
+            @click="setQuickPeriod('today')"
+            size="small"
+            elevation="0"
+          >오늘</v-btn>
+          <v-btn 
+            :class="{ 'active': selectedPeriod === 'week' }"
+            @click="setQuickPeriod('week')"
+            size="small"
+            elevation="0"
+          >7일</v-btn>
+          <v-btn 
+            :class="{ 'active': selectedPeriod === 'month' }"
+            @click="setQuickPeriod('month')"
+            size="small"
+            elevation="0"
+          >1개월</v-btn>
+          <v-btn 
+            :class="{ 'active': selectedPeriod === 'quarter' }"
+            @click="setQuickPeriod('quarter')"
+            size="small"
+            elevation="0"
+          >3개월</v-btn>
+        </div>
+      </div>
+    </div>
+
+    <!-- KPI 카드 섹션 -->
+    <div class="dashboard--kpi">
+      <div class="kpi--cards">
+        <div class="kpi--card">
+          <div class="kpi--header">
+            <h3>총 매출</h3>
+            <i class="icon sales"></i>
+          </div>
+          <div class="kpi--value">{{ formatCurrency(metrics.totalSales) }}</div>
+          <div class="kpi--change" :class="{ 'positive': metrics.salesChange >= 0, 'negative': metrics.salesChange < 0 }">
+            {{ metrics.salesChange >= 0 ? '+' : '' }}{{ metrics.salesChange }}% 전월 대비
+          </div>
+        </div>
+        <div class="kpi--card">
+          <div class="kpi--header">
+            <h3>총 주문</h3>
+            <i class="icon orders"></i>
+          </div>
+          <div class="kpi--value">{{ metrics.totalOrders }}개</div>
+          <div class="kpi--change" :class="{ 'positive': metrics.ordersChange >= 0, 'negative': metrics.ordersChange < 0 }">
+            {{ metrics.ordersChange >= 0 ? '+' : '' }}{{ metrics.ordersChange }}% 전월 대비
+          </div>
+        </div>
+        <div class="kpi--card">
+          <div class="kpi--header">
+            <h3>정산 완료율</h3>
+            <i class="icon settlement"></i>
+          </div>
+          <div class="kpi--value">{{ metrics.settlementRate }}%</div>
+          <div class="kpi--change" :class="{ 'positive': metrics.settlementChange >= 0, 'negative': metrics.settlementChange < 0 }">
+            {{ metrics.settlementChange >= 0 ? '+' : '' }}{{ metrics.settlementChange }}%p 전월 대비
+          </div>
+        </div>
+        <div class="kpi--card">
+          <div class="kpi--header">
+            <h3>평균 주문액</h3>
+            <i class="icon avg"></i>
+          </div>
+          <div class="kpi--value">{{ formatCurrency(metrics.avgOrderValue) }}</div>
+          <div class="kpi--change" :class="{ 'positive': metrics.avgOrderChange >= 0, 'negative': metrics.avgOrderChange < 0 }">
+            {{ metrics.avgOrderChange >= 0 ? '+' : '' }}{{ metrics.avgOrderChange }}% 전월 대비
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 차트 섹션 -->
+    <div class="dashboard--charts">
+      <div class="chart--row">
+        <div class="chart--container">
+          <div class="chart--header">
+            <h3>매출 추이</h3>
+            <div class="chart--controls">
+              <v-btn-toggle v-model="salesChartType" density="compact">
+                <v-btn value="line" size="small">라인</v-btn>
+                <v-btn value="bar" size="small">막대</v-btn>
+              </v-btn-toggle>
+            </div>
+          </div>
+          <div class="chart--content">
+            <canvas ref="salesChart" :key="salesChartKey"></canvas>
+          </div>
+        </div>
+        <div class="chart--container">
+          <div class="chart--header">
+            <h3>주문 현황</h3>
+          </div>
+          <div class="chart--content">
+            <canvas ref="ordersChart" :key="ordersChartKey"></canvas>
+          </div>
+        </div>
+      </div>
+      <div class="chart--row">
+        <div class="chart--container">
+          <div class="chart--header">
+            <h3>정산 현황</h3>
+          </div>
+          <div class="chart--content">
+            <canvas ref="settlementChart" :key="settlementChartKey"></canvas>
+          </div>
+        </div>
+        <div class="chart--container">
+          <div class="chart--header">
+            <h3>카테고리별 매출</h3>
+          </div>
+          <div class="chart--content">
+            <canvas ref="categoryChart" :key="categoryChartKey"></canvas>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 상세 데이터 테이블 -->
+    <div class="dashboard--table">
+      <div class="table--header">
+        <h3>최근 주문 내역</h3>
+        <v-btn 
+          color="primary" 
+          size="small"
+          @click="exportData"
+        >
+          <i class="mdi mdi-download"></i>
+          엑셀 다운로드
+        </v-btn>
+      </div>
+      <div class="table--content">
+        <v-data-table
+          :headers="tableHeaders"
+          :items="recentOrders"
+          :loading="loading"
+          class="elevation-1"
+          items-per-page="10"
+        >
+          <template v-slot:item.amount="{ item }">
+            {{ formatCurrency(item.amount) }}
+          </template>
+          <template v-slot:item.status="{ item }">
+            <v-chip
+              :color="getStatusColor(item.status)"
+              size="small"
+            >
+              {{ getStatusText(item.status) }}
+            </v-chip>
+          </template>
+          <template v-slot:item.actions="{ item }">
+            <v-btn
+              icon="mdi-eye"
+              size="small"
+              @click="viewOrder(item)"
+            ></v-btn>
+          </template>
+        </v-data-table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import VueDatePicker from "@vuepic/vue-datepicker";
+  import "@vuepic/vue-datepicker/dist/main.css";
+  import {
+    Chart as ChartJS,
+    CategoryScale,
+    LinearScale,
+    PointElement,
+    LineElement,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+    ArcElement,
+  } from 'chart.js';
+  
+  ChartJS.register(
+    CategoryScale,
+    LinearScale,
+    PointElement,
+    LineElement,
+    BarElement,
+    Title,
+    Tooltip,
+    Legend,
+    ArcElement
+  );
+
+  /************************************************************************
+|    레이아웃
+************************************************************************/
+  definePageMeta({
+    layout: "default",
+  });
+
+  /************************************************************************
+|    스토어
+ ************************************************************************/
+  const useDtStore = useDetailStore();
+
+  /************************************************************************
+|    전역 변수
+ ************************************************************************/
+  const { $toast, $log, $dayjs } = useNuxtApp();
+  const router = useRouter();
+  const pageId = ref("대시보드");
+  const dateFormat = "yyyy-MM-dd";
+  const loading = ref(false);
+
+  // 필터링 관련
+  const dateRange = ref([]);
+  const selectedPeriod = ref('month');
+
+  // 차트 관련
+  const salesChart = ref(null);
+  const ordersChart = ref(null);
+  const settlementChart = ref(null);
+  const categoryChart = ref(null);
+  const salesChartType = ref('line');
+  const salesChartKey = ref(0);
+  const ordersChartKey = ref(0);
+  const settlementChartKey = ref(0);
+  const categoryChartKey = ref(0);
+
+  // 메트릭 데이터
+  const metrics = ref({
+    totalSales: 125000000,
+    salesChange: 12.5,
+    totalOrders: 1250,
+    ordersChange: 8.3,
+    settlementRate: 95.2,
+    settlementChange: 2.1,
+    avgOrderValue: 100000,
+    avgOrderChange: 5.7
+  });
+
+  // 테이블 관련
+  const tableHeaders = ref([
+    { title: '주문번호', key: 'orderNo', sortable: true },
+    { title: '인플루언서', key: 'influencer', sortable: true },
+    { title: '상품명', key: 'productName', sortable: false },
+    { title: '주문금액', key: 'amount', sortable: true },
+    { title: '주문일', key: 'orderDate', sortable: true },
+    { title: '상태', key: 'status', sortable: true },
+    { title: '액션', key: 'actions', sortable: false }
+  ]);
+
+  const recentOrders = ref([
+    {
+      orderNo: 'ORD-2025001',
+      influencer: '김인플루',
+      productName: '프리미엄 화장품 세트',
+      amount: 150000,
+      orderDate: '2025-01-15',
+      status: 'completed'
+    },
+    {
+      orderNo: 'ORD-2025002',
+      influencer: '박유튜버',
+      productName: '건강기능식품',
+      amount: 85000,
+      orderDate: '2025-01-14',
+      status: 'shipping'
+    },
+    {
+      orderNo: 'ORD-2025003',
+      influencer: '이인스타',
+      productName: '패션 액세서리',
+      amount: 65000,
+      orderDate: '2025-01-13',
+      status: 'pending'
+    }
+  ]);
+
+  /************************************************************************
+|    함수(METHODS)
+************************************************************************/
+
+  // 통화 포맷팅
+  const formatCurrency = (amount) => {
+    return new Intl.NumberFormat('ko-KR', {
+      style: 'currency',
+      currency: 'KRW'
+    }).format(amount);
+  };
+
+  // 날짜 범위 변경
+  const onDateRangeChange = (range) => {
+    selectedPeriod.value = 'custom';
+    fetchDashboardData();
+  };
+
+  // 빠른 기간 선택
+  const setQuickPeriod = (period) => {
+    selectedPeriod.value = period;
+    const today = new Date();
+    
+    switch (period) {
+      case 'today':
+        dateRange.value = [today, today];
+        break;
+      case 'week':
+        const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
+        dateRange.value = [weekAgo, today];
+        break;
+      case 'month':
+        const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
+        dateRange.value = [monthAgo, today];
+        break;
+      case 'quarter':
+        const quarterAgo = new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000);
+        dateRange.value = [quarterAgo, today];
+        break;
+    }
+    fetchDashboardData();
+  };
+
+  // 상태 색상
+  const getStatusColor = (status) => {
+    switch (status) {
+      case 'completed': return 'success';
+      case 'shipping': return 'info';
+      case 'pending': return 'warning';
+      case 'cancelled': return 'error';
+      default: return 'grey';
+    }
+  };
+
+  // 상태 텍스트
+  const getStatusText = (status) => {
+    switch (status) {
+      case 'completed': return '완료';
+      case 'shipping': return '배송중';
+      case 'pending': return '대기';
+      case 'cancelled': return '취소';
+      default: return '알 수 없음';
+    }
+  };
+
+  // 주문 상세 보기
+  const viewOrder = (order) => {
+    router.push({
+      path: `/view/order/detail/${order.orderNo}`
+    });
+  };
+
+  // 엑셀 다운로드
+  const exportData = () => {
+    // 엑셀 다운로드 로직
+    $toast.success('데이터 다운로드를 시작합니다.');
+  };
+
+  // 차트 생성
+  const createSalesChart = () => {
+    if (!salesChart.value) return;
+    
+    const ctx = salesChart.value.getContext('2d');
+    new ChartJS(ctx, {
+      type: salesChartType.value,
+      data: {
+        labels: ['1월', '2월', '3월', '4월', '5월', '6월'],
+        datasets: [{
+          label: '매출',
+          data: [12000000, 15000000, 13000000, 18000000, 16000000, 20000000],
+          borderColor: '#3f51b5',
+          backgroundColor: salesChartType.value === 'bar' ? '#3f51b5' : 'rgba(63, 81, 181, 0.1)',
+          borderWidth: 2,
+          fill: salesChartType.value === 'line'
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          legend: {
+            display: false
+          }
+        },
+        scales: {
+          y: {
+            beginAtZero: true,
+            ticks: {
+              callback: function(value) {
+                return formatCurrency(value);
+              }
+            }
+          }
+        }
+      }
+    });
+  };
+
+  const createOrdersChart = () => {
+    if (!ordersChart.value) return;
+    
+    const ctx = ordersChart.value.getContext('2d');
+    new ChartJS(ctx, {
+      type: 'doughnut',
+      data: {
+        labels: ['신규 주문', '처리중', '배송중', '완료'],
+        datasets: [{
+          data: [45, 25, 20, 10],
+          backgroundColor: ['#4caf50', '#ff9800', '#2196f3', '#9c27b0'],
+          borderWidth: 0
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          legend: {
+            position: 'bottom'
+          }
+        }
+      }
+    });
+  };
+
+  const createSettlementChart = () => {
+    if (!settlementChart.value) return;
+    
+    const ctx = settlementChart.value.getContext('2d');
+    new ChartJS(ctx, {
+      type: 'bar',
+      data: {
+        labels: ['1주', '2주', '3주', '4주'],
+        datasets: [{
+          label: '정산 완료',
+          data: [95, 92, 98, 94],
+          backgroundColor: '#4caf50'
+        }, {
+          label: '정산 대기',
+          data: [5, 8, 2, 6],
+          backgroundColor: '#ff9800'
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        scales: {
+          x: {
+            stacked: true
+          },
+          y: {
+            stacked: true,
+            beginAtZero: true,
+            max: 100,
+            ticks: {
+              callback: function(value) {
+                return value + '%';
+              }
+            }
+          }
+        }
+      }
+    });
+  };
+
+  const createCategoryChart = () => {
+    if (!categoryChart.value) return;
+    
+    const ctx = categoryChart.value.getContext('2d');
+    new ChartJS(ctx, {
+      type: 'pie',
+      data: {
+        labels: ['화장품', '패션', '건강식품', '전자제품', '기타'],
+        datasets: [{
+          data: [35, 25, 20, 15, 5],
+          backgroundColor: ['#e91e63', '#9c27b0', '#3f51b5', '#009688', '#ff5722']
+        }]
+      },
+      options: {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+          legend: {
+            position: 'right'
+          }
+        }
+      }
+    });
+  };
+
+  // 대시보드 데이터 가져오기
+  const fetchDashboardData = async () => {
+    loading.value = true;
+    
+    try {
+      const _req = {
+        compId: useAuthStore().getCompanyId,
+        startDate: dateRange.value[0],
+        endDate: dateRange.value[1]
+      };
+
+      await useAxios()
+        .post("/dashboard/metrics", _req)
+        .then((res) => {
+          if (res.data) {
+            metrics.value = res.data;
+          }
+        });
+    } catch (error) {
+      $toast.error('대시보드 데이터를 불러오는데 실패했습니다.');
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  // 차트 타입 변경 감지
+  watch(salesChartType, () => {
+    salesChartKey.value++;
+    nextTick(() => {
+      createSalesChart();
+    });
+  });
+
+  /************************************************************************
+|    라이프사이클
+************************************************************************/
+
+  onMounted(() => {
+    // 기본 1개월 기간 설정
+    setQuickPeriod('month');
+    
+    nextTick(() => {
+      createSalesChart();
+      createOrdersChart();
+      createSettlementChart();
+      createCategoryChart();
+    });
+  });
+</script>
+
+<style scoped>
+/* 대시보드 필터 스타일 */
+.dashboard--filters {
+  margin: 20px 0;
+  padding: 20px;
+  background: #f8f9fa;
+  border-radius: 8px;
+}
+
+.filter--wrap {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  flex-wrap: wrap;
+}
+
+.date--range {
+  min-width: 300px;
+}
+
+.quick--filters {
+  display: flex;
+  gap: 8px;
+}
+
+.quick--filters .v-btn {
+  background: white;
+  color: #666;
+  border: 1px solid #ddd;
+}
+
+.quick--filters .v-btn.active {
+  background: #3f51b5;
+  color: white;
+  border-color: #3f51b5;
+}
+
+/* KPI 카드 스타일 */
+.dashboard--kpi {
+  margin: 20px 0;
+}
+
+.kpi--cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 20px;
+}
+
+.kpi--card {
+  background: white;
+  padding: 24px;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  border: 1px solid #e0e0e0;
+}
+
+.kpi--header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.kpi--header h3 {
+  font-size: 14px;
+  font-weight: 600;
+  color: #666;
+  margin: 0;
+}
+
+.kpi--header .icon {
+  width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.icon.sales {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.icon.orders {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.icon.settlement {
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.icon.avg {
+  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.kpi--value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #333;
+  margin-bottom: 8px;
+}
+
+.kpi--change {
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.kpi--change.positive {
+  color: #4caf50;
+}
+
+.kpi--change.negative {
+  color: #f44336;
+}
+
+/* 차트 스타일 */
+.dashboard--charts {
+  margin: 30px 0;
+}
+
+.chart--row {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
+  gap: 20px;
+  margin-bottom: 20px;
+}
+
+.chart--container {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  border: 1px solid #e0e0e0;
+}
+
+.chart--header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.chart--header h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  margin: 0;
+}
+
+.chart--content {
+  height: 300px;
+  position: relative;
+}
+
+.chart--content canvas {
+  max-height: 100%;
+}
+
+/* 테이블 스타일 */
+.dashboard--table {
+  background: white;
+  border-radius: 12px;
+  padding: 24px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  border: 1px solid #e0e0e0;
+  margin: 20px 0;
+}
+
+.table--header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.table--header h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  margin: 0;
+}
+
+/* 반응형 */
+@media (max-width: 768px) {
+  .filter--wrap {
+    flex-direction: column;
+    align-items: stretch;
+  }
+  
+  .date--range {
+    min-width: auto;
+  }
+  
+  .quick--filters {
+    justify-content: center;
+  }
+  
+  .kpi--cards {
+    grid-template-columns: 1fr;
+  }
+  
+  .chart--row {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 123 - 148
pages/view/vendor/index.vue

@@ -8,23 +8,55 @@
       </div>
     </div>
 
-    <div class="search--modules">
-      <div class="form--cont--filter">
-        <v-select
-          v-model="filter"
-          :items="filderArr"
-          variant="outlined"
-          class="custom-select"
-        >
-        </v-select>
+    <div class="search--modules type2">
+      <div class="search--inner">
+        <div class="form--cont--filter">
+          <v-select
+            v-model="filter"
+            :items="filderArr"
+            variant="outlined"
+            class="custom-select"
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--text">
+          <v-text-field
+            v-model="searchModel"
+            class="custom-input mini"
+            style="width: 100%"
+            placeholder="검색어를 입력하세요"
+          ></v-text-field>
+        </div>
       </div>
-      <div class="form--cont--text">
-        <v-text-field
-          v-model="searchModel"
-          class="custom-input mini"
-          style="width: 100%"
-          placeholder="검색어를 입력하세요"
-        ></v-text-field>
+      <div class="search--inner">
+        <div class="calendar-wrap ml--0">
+          <div class="calendar">
+            <VueDatePicker
+              :format="datePickerFormat"
+              v-model="searchStartDate"
+              placeholder="날짜를 선택하세요"
+              :auto-apply="true"
+              week-start="0"
+            ></VueDatePicker>
+          </div>
+          <span class="text">~</span>
+          <div class="calendar">
+            <VueDatePicker
+              v-model="searchEndDate"
+              :format="datePickerFormat"
+              placeholder="날짜를 선택하세요"
+              :auto-apply="true"
+              week-start="0"
+            ></VueDatePicker>
+          </div>
+          <div class="month--selector">
+            <v-btn elevation="0">오늘</v-btn>
+            <v-btn class="actv" elevation="0">7일</v-btn>
+            <v-btn elevation="0">1개월</v-btn>
+            <v-btn elevation="0">3개월</v-btn>
+            <v-btn elevation="0">전체</v-btn>
+          </div>
+        </div>
       </div>
       <v-btn
         class="custom-btn btn-blue mini sch--btn"
@@ -39,52 +71,52 @@
           <!-- <v-btn class="custom-btn mini btn-white">선택 삭제</v-btn> -->
         </div>
         <div class="right--sections">
-          <!-- <v-btn class="custom-btn mini btn-reg" @click="addLocated()"
-            ><i class="ico"></i>신규 등록</v-btn
-          > -->
+          <v-btn class="custom-btn mini btn-blue mr-2" @click="goToVendorsList()"
+            ><i class="ico"></i>벤더사 관리</v-btn
+          >
+          <v-btn class="custom-btn mini btn-reg" @click="addLocated()"
+            ><i class="ico"></i>제품 등록</v-btn
+          >
         </div>
       </div>
-
-      <div class="tbl-wrapper">
-        <div class="tbl-wrap">
-          <!-- ag grid -->
-          <ag-grid-vue
-            style="width: 100%; height: calc(10 * 2.94rem)"
-            class="ag-theme-quartz"
-            :gridOptions="gridOptions"
-            :rowData="tblItems"
-            :paginationPageSize="pageObj.pageSize"
-            :suppressPaginationPanel="true"
-            @grid-ready="onGridReady"
-            @rowClicked="detailLocated"
+      <div class="item--list--wrap" v-if="itemList.length > 0">
+        <div class="item--list">
+          <div
+            v-for="(items, index) in paginatedItems"
+            :key="index"
+            @click="toItemDetail(items.SEQ)"
+            class="item"
           >
-          </ag-grid-vue>
-
-          <!-- 페이징 -->
-          <div class="ag-grid-custom-pagenations">
-            <pagination @chg_page="chgPage" :pageObj="pageObj"></pagination>
+            <div class="item--img"></div>
+            <h3>{{ items.NAME }}</h3>
+            <p>공급가: {{ items.PRICE1 }}<br />판매가: {{ items.PRICE2 }}</p>
+            <span>등록일: {{ items.REGDATE.slice(0, 10) }}</span>
+            <div v-show="items.STATUS == 1" class="sold--out"><span>품절</span></div>
           </div>
         </div>
+        <div class="item--pagination">
+          <v-pagination
+            v-model="currentPage"
+            :length="Math.ceil(itemList.length / itemsPerPage)"
+          ></v-pagination>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-  import pagination from "../components/common/pagination.vue";
-  import { AgGridVue } from "ag-grid-vue3";
-  import searchModules from "@/components/search/searchModules";
-  import customElements from "@/components/cellRenderer/customTextColor";
-  import { watch } from "vue";
+  import VueDatePicker from "@vuepic/vue-datepicker";
+  import "@vuepic/vue-datepicker/dist/main.css";
   /************************************************************************
-  |    레이아웃
-  ************************************************************************/
+|    레이아웃
+************************************************************************/
   definePageMeta({
     layout: "default",
   });
   /************************************************************************
-  |   PROPS
-   ************************************************************************/
+|   PROPS
+ ************************************************************************/
   const props = defineProps({
     propsData: {
       type: Object,
@@ -92,139 +124,90 @@
     },
   });
   /************************************************************************
-  |    스토어
-   ************************************************************************/
+|    스토어
+ ************************************************************************/
   const useDtStore = useDetailStore();
   /************************************************************************
-  |    전역
-   ************************************************************************/
+|    전역
+ ************************************************************************/
+  const searchModel = ref("");
+  const searchStartDate = ref("");
+  const searchEndDate = ref("");
   const filter = ref("");
-  const evtDate = ref("");
   const filderArr = ref([
     { title: "선택하세요", value: "" },
-    { title: "이름", value: "name" },
-    { title: "아이디", value: "id" },
+    { title: "제목", value: "title" },
   ]);
   const { $toast, $log, $dayjs, $eventBus } = useNuxtApp();
   const router = useRouter();
-  const pageId = ref("당첨자 리스트");
-  let pageObj = ref({
-    page: 1, // 현재 페이지
-    pageMaxNumSize: 10, // 페이지 숫자 최대 표현 개수
-    pageSize: 10, // 테이블 조회 데이터 개수
-    totalCnt: 0, // 전체 페이지
-  });
-  const tblItems = ref([]); // stat 데이터
+  const pageId = ref("제품 관리");
+  const datePickerFormat = "yyyy-MM-dd";
+  const itemList = ref([]);
+  const itemsPerPage = 5;
+  const currentPage = ref(1);
 
   /* eslint-disable */
   /* prettier-ignore */
 
-  pageObj.value.totalCnt = tblItems.value.length;
-
-  const remToPx = () => parseFloat(getComputedStyle(document.documentElement).fontSize);
-  const rowHeightRem = 2.65; // 원하는 rem 값
-  const rowHeightPx = rowHeightRem * remToPx();
-  const gridApi = shallowRef();
-
-  // gridOption
-  const gridOptions = {
-    columnDefs: [
-      {
-        headerName: "No",
-        valueGetter: (params) => params.api.getDisplayedRowCount() - params.node.rowIndex,
-        sortable: false,
-        width: 70,
-      },
-      // { headerName: "번호", field: "NO", sortable: false },
-      { headerName: "제목", field: "TITLE", sortable: false },
-      { headerName: "기간", field: "EVTDATE", sortable: false },
-      { headerName: "상태", field: "STATUS", sortable: false, width: 140 },
-      { headerName: "등록일", field: "REGDATE", sortable: false, width: 140 },
-      // {
-      //   headerName: "알림 메일 수신 여부",
-      //   field: "mail_recp_yn",
-      //   sortable: false,
-      //   width: 130,
-      // },
-    ],
-    rowData: tblItems.value, // 테이블 데이터
-    autoSizeStrategy: {
-      type: "fitGridWidth", // width맞춤
-    },
-    suppressMovableColumns: true,
-    headerHeight: rowHeightPx,
-    rowHeight: rowHeightPx,
-    pagination: true,
-    suppressPaginationPanel: true, // 하단 default 페이징 컨트롤 숨김
-    //rowSelection: {
-    // checkboxes: true,
-    // headerCheckbox: true,
-    // enableClickSelection: false,
-    // mode: "multiRow",
-    //},
-  };
-
   /************************************************************************
-  |    함수(METHODS)
-  ************************************************************************/
-  const onGridReady = (__PARAMS) => {
-    gridApi.value = __PARAMS.api;
-  };
+|    함수(METHODS)
+************************************************************************/
 
-  const chgPage = (__PAGE) => {
-    pageObj.value.page = __PAGE;
-    gridApi.value.paginationGoToPage(__PAGE - 1);
-  };
+  const paginatedItems = computed(() => {
+    const start = (currentPage.value - 1) * itemsPerPage;
+    return itemList.value.slice(start, start + itemsPerPage);
+  });
 
   const addLocated = () => {
     router.push({
-      path: "/view/winner/winDetail",
+      path: "/view/vendor/product-register",
     });
-    useDtStore.adminInfo.pageType = "I";
   };
 
-  const detailLocated = (__EVENT) => {
+  const goToVendorsList = () => {
     router.push({
-      path: "/view/winner/winDetail",
+      path: "/view/vendor/vendors",
     });
+  };
 
+  const detailLocated = (__EVENT) => {
+    router.push({
+      path: "/view/item/detail",
+    });
     useDtStore.boardInfo.seq = __EVENT.data.SEQ;
-    useDtStore.adminInfo.pageType = "U";
+    useDtStore.boardInfo.pageType = "U";
+    useDtStore.boardInfo.status = __EVENT.data.STATUS;
   };
 
-  const winnerList = (__STATUS) => {
+  const evtListGet = async () => {
     let _req = {
       compId: useAuthStore().getCompanyId,
-      _size: 1000,
-      _index: 0,
-      status: __STATUS,
+      status: null,
     };
 
-    useAxios()
-      .post("/winner/list", _req)
+    await useAxios()
+      .post("/item/list", _req)
       .then((res) => {
-        // STARTDATE와 ENDDATE를 합쳐 EVTDATE 생성
-        const processedData = res.data.map((item) => ({
-          ...item,
-          EVTDATE: `${item.STARTDATE || ""} ~ ${item.ENDDATE || ""}`,
-        }));
-        _req._size = processedData.length;
-        tblItems.value = processedData;
-        pageObj.value.totalCnt = tblItems.value.length;
+        itemList.value = res.data;
+        //pageTotal.value = res.data._total_cnt;
       });
   };
 
   const fnSearch = (__KEYWORD, __FILTER) => {
     let _req = {
       compId: useAuthStore().getCompanyId,
-      _size: 1000,
-      _index: 0,
       filter: __FILTER,
       keyword: __KEYWORD,
+      status: "",
+      // _size: 1000,
+      // _index: 0,
+      // admin_name:
+      //   __FILTER == "admin_name" ? __KEYWORD : __FILTER == "" ? __KEYWORD : null,
+      // id: __FILTER == "id" ? __KEYWORD : __FILTER == "" ? __KEYWORD : null,
     };
 
     useAxios()
-      .post("/mng/search", _req)
+      .post("/evt/search", _req)
       .then((res) => {
         _req._size = res.data.length;
 
@@ -235,8 +218,8 @@
   };
 
   /************************************************************************
-  |    WATCH
-  ************************************************************************/
+|    WATCH
+************************************************************************/
 
   watch(
     () => props,
@@ -248,14 +231,6 @@
   );
 
   onMounted(() => {
-    winnerList(useDtStore.menuInfo.pageStatus);
+    evtListGet();
   });
-
-  // 스토어의 pageStatus가 변경될 때마다 winnerList 호출
-  watch(
-    () => useDtStore.menuInfo.pageStatus,
-    (newStatus) => {
-      winnerList(newStatus);
-    }
-  );
 </script>

+ 594 - 0
pages/view/vendor/product-register.vue

@@ -0,0 +1,594 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>제품 관리</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <div class="product-register--form">
+      <div class="form--container">
+        <div class="form--section">
+          <h3>기본 정보</h3>
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">제품명 <span class="required">*</span></label>
+              <v-text-field
+                v-model="form.productName"
+                placeholder="제품명을 입력하세요"
+                class="custom-input"
+                :rules="[rules.required]"
+              ></v-text-field>
+            </div>
+          </div>
+
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">공급가 <span class="required">*</span></label>
+              <v-text-field
+                v-model="form.supplyPrice"
+                type="number"
+                placeholder="공급가를 입력하세요"
+                class="custom-input"
+                :rules="[rules.required]"
+              ></v-text-field>
+            </div>
+            <div class="form--field">
+              <label class="form--label">판매가 <span class="required">*</span></label>
+              <v-text-field
+                v-model="form.sellPrice"
+                type="number"
+                placeholder="판매가를 입력하세요"
+                class="custom-input"
+                :rules="[rules.required]"
+              ></v-text-field>
+            </div>
+          </div>
+
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">배송비 <span class="required">*</span></label>
+              <v-text-field
+                v-model="form.shippingCost"
+                placeholder="배송비 정보를 입력하세요 (예: 3,000원, 무료배송)"
+                class="custom-input"
+                :rules="[rules.required]"
+              ></v-text-field>
+            </div>
+          </div>
+
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">소타이틀</label>
+              <v-text-field
+                v-model="form.subtitle"
+                placeholder="소타이틀을 입력하세요"
+                class="custom-input"
+              ></v-text-field>
+            </div>
+          </div>
+        </div>
+
+        <div class="form--section">
+          <h3>상세 정보</h3>
+          <div class="form--row">
+            <div class="form--field full-width">
+              <label class="form--label">상세내용</label>
+              <div class="editor--container">
+                <div class="editor--toolbar">
+                  <v-btn-toggle v-model="editorMode" density="compact" class="editor--mode-toggle">
+                    <v-btn value="html" size="small">HTML</v-btn>
+                    <v-btn value="text" size="small">TEXT</v-btn>
+                  </v-btn-toggle>
+                </div>
+                <div v-show="editorMode === 'html'" class="html-editor">
+                  <textarea
+                    v-model="form.detailContent"
+                    placeholder="HTML 내용을 입력하세요"
+                    class="html-textarea"
+                    rows="15"
+                  ></textarea>
+                </div>
+                <div v-show="editorMode === 'text'" class="text-editor">
+                  <v-textarea
+                    v-model="form.detailContent"
+                    placeholder="텍스트 내용을 입력하세요"
+                    class="custom-input"
+                    rows="15"
+                  ></v-textarea>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="form--section">
+          <h3>첨부파일</h3>
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">상세다운로드</label>
+              <div class="file-upload--container">
+                <input
+                  type="file"
+                  ref="fileInput"
+                  @change="handleFileUpload"
+                  accept=".zip"
+                  class="file-input"
+                  style="display: none"
+                />
+                <div class="file-upload--area" @click="triggerFileUpload">
+                  <div v-if="!form.detailFile" class="file-upload--placeholder">
+                    <i class="upload-icon">📎</i>
+                    <p>ZIP 파일을 선택하세요</p>
+                    <small>클릭하여 파일 선택</small>
+                  </div>
+                  <div v-else class="file-upload--selected">
+                    <i class="file-icon">📁</i>
+                    <div class="file-info">
+                      <p class="file-name">{{ form.detailFile.name }}</p>
+                      <small class="file-size">{{ formatFileSize(form.detailFile.size) }}</small>
+                    </div>
+                    <v-btn
+                      @click.stop="removeFile"
+                      class="file-remove"
+                      size="small"
+                      color="error"
+                      icon="mdi-close"
+                    ></v-btn>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="form--section">
+          <h3>상태 설정</h3>
+          <div class="form--row">
+            <div class="form--field">
+              <label class="form--label">상태 <span class="required">*</span></label>
+              <v-select
+                v-model="form.status"
+                :items="statusOptions"
+                variant="outlined"
+                class="custom-select"
+                :rules="[rules.required]"
+              ></v-select>
+            </div>
+            <div class="form--field">
+              <label class="form--label">노출상태 <span class="required">*</span></label>
+              <v-select
+                v-model="form.displayStatus"
+                :items="displayStatusOptions"
+                variant="outlined"
+                class="custom-select"
+                :rules="[rules.required]"
+              ></v-select>
+            </div>
+          </div>
+        </div>
+
+        <div class="form--section">
+          <h3>업데이트 내역</h3>
+          <div class="form--row">
+            <div class="form--field full-width">
+              <label class="form--label">업데이트 내역</label>
+              <v-textarea
+                v-model="form.updateHistory"
+                placeholder="업데이트 내역을 입력하세요 (최대 500자)"
+                class="custom-input"
+                rows="5"
+                :counter="500"
+                :rules="[rules.maxLength(500)]"
+              ></v-textarea>
+            </div>
+          </div>
+        </div>
+
+        <div class="form--actions">
+          <v-btn
+            class="custom-btn btn-white"
+            @click="goBack"
+          >
+            취소
+          </v-btn>
+          <v-btn
+            class="custom-btn btn-blue"
+            @click="saveProduct"
+            :loading="loading"
+          >
+            저장
+          </v-btn>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+/************************************************************************
+|    레이아웃
+************************************************************************/
+definePageMeta({
+  layout: "default",
+});
+
+/************************************************************************
+|    스토어
+************************************************************************/
+const useDtStore = useDetailStore();
+
+/************************************************************************
+|    전역 변수
+************************************************************************/
+const { $toast, $log } = useNuxtApp();
+const router = useRouter();
+const pageId = ref("제품 등록");
+const loading = ref(false);
+const editorMode = ref("text");
+const fileInput = ref(null);
+
+/************************************************************************
+|    폼 데이터
+************************************************************************/
+const form = ref({
+  productName: "",
+  supplyPrice: "",
+  sellPrice: "",
+  shippingCost: "",
+  subtitle: "",
+  detailContent: "",
+  detailFile: null,
+  status: "판매중",
+  displayStatus: "노출",
+  updateHistory: ""
+});
+
+/************************************************************************
+|    옵션 데이터
+************************************************************************/
+const statusOptions = ref([
+  { title: "판매중", value: "판매중" },
+  { title: "품절", value: "품절" }
+]);
+
+const displayStatusOptions = ref([
+  { title: "노출", value: "노출" },
+  { title: "비노출", value: "비노출" }
+]);
+
+/************************************************************************
+|    유효성 검사
+************************************************************************/
+const rules = {
+  required: (value) => !!value || "필수 입력 항목입니다.",
+  maxLength: (max) => (value) => {
+    if (!value) return true;
+    return value.length <= max || `최대 ${max}자까지 입력 가능합니다.`;
+  }
+};
+
+/************************************************************************
+|    함수(METHODS)
+************************************************************************/
+
+// 파일 업로드 트리거
+const triggerFileUpload = () => {
+  fileInput.value.click();
+};
+
+// 파일 업로드 처리
+const handleFileUpload = (event) => {
+  const file = event.target.files[0];
+  if (file) {
+    if (file.type !== 'application/zip' && !file.name.endsWith('.zip')) {
+      $toast.error('ZIP 파일만 업로드 가능합니다.');
+      return;
+    }
+    
+    form.value.detailFile = file;
+    $toast.success('파일이 선택되었습니다.');
+  }
+};
+
+// 파일 제거
+const removeFile = () => {
+  form.value.detailFile = null;
+  if (fileInput.value) {
+    fileInput.value.value = '';
+  }
+};
+
+// 파일 크기 포맷팅
+const formatFileSize = (bytes) => {
+  if (bytes === 0) return '0 Bytes';
+  const k = 1024;
+  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+  const i = Math.floor(Math.log(bytes) / Math.log(k));
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
+
+// 뒤로가기
+const goBack = () => {
+  router.push('/view/vendor');
+};
+
+// 제품 저장
+const saveProduct = async () => {
+  // 유효성 검사
+  if (!form.value.productName) {
+    $toast.error('제품명을 입력하세요.');
+    return;
+  }
+  
+  if (!form.value.supplyPrice) {
+    $toast.error('공급가를 입력하세요.');
+    return;
+  }
+  
+  if (!form.value.sellPrice) {
+    $toast.error('판매가를 입력하세요.');
+    return;
+  }
+  
+  if (!form.value.shippingCost) {
+    $toast.error('배송비를 입력하세요.');
+    return;
+  }
+
+  loading.value = true;
+  
+  try {
+    // FormData 생성 (파일 업로드용)
+    const formData = new FormData();
+    formData.append('productName', form.value.productName);
+    formData.append('supplyPrice', form.value.supplyPrice);
+    formData.append('sellPrice', form.value.sellPrice);
+    formData.append('shippingCost', form.value.shippingCost);
+    formData.append('subtitle', form.value.subtitle);
+    formData.append('detailContent', form.value.detailContent);
+    formData.append('status', form.value.status);
+    formData.append('displayStatus', form.value.displayStatus);
+    formData.append('updateHistory', form.value.updateHistory);
+    formData.append('compId', useAuthStore().getCompanyId);
+    
+    if (form.value.detailFile) {
+      formData.append('detailFile', form.value.detailFile);
+    }
+
+    await useAxios()
+      .post("/product/register", formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      })
+      .then((res) => {
+        if (res.data.success) {
+          $toast.success('제품이 등록되었습니다.');
+          router.push('/view/vendor');
+        } else {
+          $toast.error('제품 등록에 실패했습니다.');
+        }
+      });
+  } catch (error) {
+    $log.error('제품 등록 오류:', error);
+    $toast.error('제품 등록 중 오류가 발생했습니다.');
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped>
+.product-register--form {
+  margin: 20px 0;
+}
+
+.form--container {
+  background: white;
+  border-radius: 8px;
+  padding: 30px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.form--section {
+  margin-bottom: 40px;
+}
+
+.form--section h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20px;
+  padding-bottom: 10px;
+  border-bottom: 2px solid #f0f0f0;
+}
+
+.form--row {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 20px;
+}
+
+.form--field {
+  flex: 1;
+}
+
+.form--field.full-width {
+  width: 100%;
+}
+
+.form--label {
+  display: block;
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 8px;
+}
+
+.form--label .required {
+  color: #f44336;
+}
+
+.custom-input {
+  width: 100%;
+}
+
+.custom-select {
+  width: 100%;
+}
+
+/* 에디터 스타일 */
+.editor--container {
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.editor--toolbar {
+  background: #f8f9fa;
+  padding: 10px;
+  border-bottom: 1px solid #ddd;
+}
+
+.editor--mode-toggle {
+  background: white;
+  border-radius: 4px;
+}
+
+.html-textarea {
+  width: 100%;
+  border: none;
+  outline: none;
+  padding: 15px;
+  font-family: 'Courier New', monospace;
+  font-size: 14px;
+  resize: vertical;
+  min-height: 400px;
+}
+
+.text-editor {
+  padding: 15px;
+}
+
+/* 파일 업로드 스타일 */
+.file-upload--container {
+  width: 100%;
+}
+
+.file-upload--area {
+  border: 2px dashed #ddd;
+  border-radius: 8px;
+  padding: 30px;
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.file-upload--area:hover {
+  border-color: #3f51b5;
+  background: #f8f9ff;
+}
+
+.file-upload--placeholder {
+  color: #666;
+}
+
+.file-upload--placeholder .upload-icon {
+  font-size: 48px;
+  display: block;
+  margin-bottom: 10px;
+}
+
+.file-upload--placeholder p {
+  font-size: 16px;
+  margin: 10px 0;
+}
+
+.file-upload--placeholder small {
+  color: #999;
+}
+
+.file-upload--selected {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding: 15px;
+  background: #f8f9fa;
+  border-radius: 4px;
+}
+
+.file-upload--selected .file-icon {
+  font-size: 24px;
+}
+
+.file-info {
+  flex: 1;
+  text-align: left;
+}
+
+.file-name {
+  font-weight: 500;
+  margin: 0;
+}
+
+.file-size {
+  color: #666;
+}
+
+.file-remove {
+  margin-left: auto;
+}
+
+/* 액션 버튼 스타일 */
+.form--actions {
+  display: flex;
+  justify-content: center;
+  gap: 15px;
+  margin-top: 40px;
+  padding-top: 20px;
+  border-top: 1px solid #e0e0e0;
+}
+
+.custom-btn {
+  padding: 12px 30px;
+  font-size: 14px;
+  font-weight: 500;
+  border-radius: 4px;
+  min-width: 120px;
+}
+
+.btn-white {
+  background: white;
+  color: #666;
+  border: 1px solid #ddd;
+}
+
+.btn-blue {
+  background: #3f51b5;
+  color: white;
+}
+
+/* 반응형 */
+@media (max-width: 768px) {
+  .form--row {
+    flex-direction: column;
+    gap: 15px;
+  }
+  
+  .form--container {
+    padding: 20px;
+  }
+  
+  .form--actions {
+    flex-direction: column;
+  }
+  
+  .custom-btn {
+    width: 100%;
+  }
+}
+</style>

+ 319 - 0
pages/view/vendor/vendors.vue

@@ -0,0 +1,319 @@
+<template>
+  <div>
+    <div class="inner--headers">
+      <h2>{{ pageId }}</h2>
+      <div class="bread--crumbs--wrap">
+        <span>홈</span>
+        <span>{{ pageId }}</span>
+      </div>
+    </div>
+
+    <!-- 검색 및 필터 영역 -->
+    <div class="search--modules type2">
+      <div class="search--inner">
+        <div class="form--cont--filter">
+          <v-select
+            v-model="selectedCategory"
+            :items="categoryOptions"
+            variant="outlined"
+            class="custom-select"
+            label="카테고리"
+            clearable
+          >
+          </v-select>
+        </div>
+        <div class="form--cont--text">
+          <v-text-field
+            v-model="searchName"
+            class="custom-input mini"
+            style="width: 100%"
+            placeholder="벤더사명을 입력하세요"
+            @keyup.enter="handleSearch"
+          ></v-text-field>
+        </div>
+      </div>
+      <v-btn
+        class="custom-btn btn-blue mini sch--btn"
+        @click="handleSearch"
+        :loading="vendorsStore.getLoading"
+      >
+        검색
+      </v-btn>
+    </div>
+
+    <!-- 벤더사 리스트 -->
+    <div class="data--list--wrap">
+      <div class="btn--actions--wrap">
+        <div class="left--sections">
+          <span class="result-count">
+            총 {{ vendorsStore.getPagination?.totalCount || 0 }}개의 벤더사
+          </span>
+        </div>
+      </div>
+
+      <!-- 로딩 상태 -->
+      <div v-if="vendorsStore.getLoading" class="loading-wrap">
+        <v-progress-circular indeterminate color="primary"></v-progress-circular>
+        <p>벤더사를 검색하고 있습니다...</p>
+      </div>
+
+      <!-- 에러 상태 -->
+      <div v-else-if="vendorsStore.getError" class="error-wrap">
+        <v-alert type="error" dismissible @click:close="vendorsStore.clearError()">
+          {{ vendorsStore.getError }}
+        </v-alert>
+      </div>
+
+      <!-- 벤더사 리스트 -->
+      <div v-else-if="vendorsStore.getVendors?.length > 0" class="vendor--list--wrap">
+        <v-data-table
+          :headers="headers"
+          :items="vendorsStore.getVendors"
+          :loading="vendorsStore.getLoading"
+          class="custom-data-table"
+          @click:row="handleRowClick"
+        >
+          <template #item.logo="{ item }">
+            <v-avatar size="40" class="vendor-logo">
+              <v-img
+                v-if="item.logo"
+                :src="item.logo"
+                :alt="item.name + ' 로고'"
+              ></v-img>
+              <div v-else class="no-logo">{{ item.name.charAt(0) }}</div>
+            </v-avatar>
+          </template>
+          
+          <template #item.category="{ item }">
+            <v-chip
+              :color="getCategoryColor(item.category)"
+              size="small"
+              variant="outlined"
+            >
+              {{ item.category }}
+            </v-chip>
+          </template>
+
+          <template #item.status="{ item }">
+            <v-chip
+              :color="item.status === 'ACTIVE' ? 'success' : 'error'"
+              size="small"
+            >
+              {{ item.status === 'ACTIVE' ? '활성' : '비활성' }}
+            </v-chip>
+          </template>
+
+          <template #item.actions="{ item }">
+            <v-btn
+              icon="mdi-eye"
+              size="small"
+              variant="text"
+              @click.stop="viewVendorDetail(item.id)"
+            >
+            </v-btn>
+          </template>
+        </v-data-table>
+
+        <!-- 페이지네이션 -->
+        <div class="pagination-wrap" v-if="(vendorsStore.getPagination?.totalPages || 0) > 1">
+          <v-pagination
+            v-model="currentPage"
+            :length="vendorsStore.getPagination?.totalPages || 1"
+            :total-visible="7"
+            @update:model-value="handlePageChange"
+          ></v-pagination>
+        </div>
+      </div>
+
+      <!-- 검색 결과 없음 -->
+      <div v-else class="no-data-wrap">
+        <div class="no-data">
+          <v-icon size="64" color="grey-lighten-1">mdi-store-search</v-icon>
+          <h3>검색된 벤더사가 없습니다</h3>
+          <p>다른 검색 조건을 시도해보세요</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { useRouter } from 'vue-router'
+import { useVendorsStore } from '@/stores/vendors'
+
+/************************************************************************
+|    레이아웃
+************************************************************************/
+definePageMeta({
+  layout: "default",
+})
+
+/************************************************************************
+|    스토어 & 라우터
+************************************************************************/
+const vendorsStore = useVendorsStore()
+const router = useRouter()
+
+/************************************************************************
+|    반응형 데이터
+************************************************************************/
+const pageId = ref("벤더사 관리")
+const searchName = ref("")
+const selectedCategory = ref("")
+const currentPage = ref(1)
+
+const categoryOptions = ref([
+  { title: "전체", value: "" },
+  { title: "패션·뷰티", value: "FASHION_BEAUTY" },
+  { title: "식품·건강", value: "FOOD_HEALTH" },
+  { title: "라이프스타일", value: "LIFESTYLE" },
+  { title: "테크·가전", value: "TECH_ELECTRONICS" },
+  { title: "스포츠·레저", value: "SPORTS_LEISURE" },
+  { title: "문화·엔터테인먼트", value: "CULTURE_ENTERTAINMENT" }
+])
+
+const headers = [
+  { title: "로고", key: "logo", sortable: false, width: "80px" },
+  { title: "벤더사명", key: "name", sortable: true },
+  { title: "카테고리", key: "category", sortable: true },
+  { title: "담당자", key: "contactName", sortable: false },
+  { title: "연락처", key: "contactPhone", sortable: false },
+  { title: "이메일", key: "contactEmail", sortable: false },
+  { title: "상태", key: "status", sortable: true },
+  { title: "등록일", key: "createdAt", sortable: true },
+  { title: "액션", key: "actions", sortable: false, width: "100px" }
+]
+
+/************************************************************************
+|    computed
+************************************************************************/
+const currentSearchConditions = computed(() => vendorsStore.getSearchConditions)
+
+/************************************************************************
+|    메서드
+************************************************************************/
+const handleSearch = async () => {
+  const conditions = {
+    name: searchName.value,
+    category: selectedCategory.value,
+    page: 1,
+    size: 10
+  }
+  
+  currentPage.value = 1
+  await vendorsStore.searchVendors(conditions)
+}
+
+const handlePageChange = async (page) => {
+  currentPage.value = page
+  const conditions = {
+    ...currentSearchConditions.value,
+    page: page
+  }
+  
+  await vendorsStore.searchVendors(conditions)
+}
+
+const handleRowClick = (event, { item }) => {
+  if (item?.id) {
+    viewVendorDetail(item.id)
+  }
+}
+
+const viewVendorDetail = (vendorId) => {
+  router.push(`/view/vendor/${vendorId}`)
+}
+
+const getCategoryColor = (category) => {
+  const colors = {
+    'FASHION_BEAUTY': 'pink',
+    'FOOD_HEALTH': 'green',
+    'LIFESTYLE': 'blue',
+    'TECH_ELECTRONICS': 'purple',
+    'SPORTS_LEISURE': 'orange',
+    'CULTURE_ENTERTAINMENT': 'red'
+  }
+  return colors[category] || 'grey'
+}
+
+/************************************************************************
+|    라이프사이클
+************************************************************************/
+onMounted(async () => {
+  // 초기 검색 실행 (전체 벤더사 로드)
+  await vendorsStore.searchVendors({
+    name: '',
+    category: '',
+    page: 1,
+    size: 10
+  })
+})
+</script>
+
+<style scoped>
+.vendor--list--wrap {
+  margin-top: 20px;
+}
+
+.loading-wrap, .error-wrap, .no-data-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+}
+
+.no-data {
+  text-align: center;
+}
+
+.no-data h3 {
+  margin: 16px 0 8px;
+  color: #666;
+}
+
+.no-data p {
+  color: #999;
+}
+
+.vendor-logo {
+  border: 1px solid #e0e0e0;
+}
+
+.no-logo {
+  background: #f5f5f5;
+  color: #666;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.result-count {
+  font-size: 14px;
+  color: #666;
+  font-weight: 500;
+}
+
+.pagination-wrap {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+}
+
+.custom-data-table {
+  background: white;
+  border-radius: 8px;
+}
+
+.custom-data-table :deep(.v-data-table__tr) {
+  cursor: pointer;
+}
+
+.custom-data-table :deep(.v-data-table__tr:hover) {
+  background-color: #f5f5f5;
+}
+</style>

+ 158 - 0
stores/vendors.js

@@ -0,0 +1,158 @@
+export const useVendorsStore = defineStore('vendorsStore', () => {
+  // State
+  const vendors = ref([])
+  const currentVendor = ref(null)
+  const loading = ref(false)
+  const error = ref(null)
+  
+  // Search & Filter State
+  const searchConditions = ref({
+    name: '',
+    category: '',
+    page: 1,
+    size: 10
+  })
+  
+  // Pagination State
+  const pagination = ref({
+    currentPage: 1,
+    pageSize: 10,
+    totalCount: 0,
+    totalPages: 0
+  })
+
+  // Getters (직접 반환)
+  const getVendors = computed(() => vendors.value)
+  const getCurrentVendor = computed(() => currentVendor.value)
+  const getLoading = computed(() => loading.value)
+  const getError = computed(() => error.value)
+  const getSearchConditions = computed(() => searchConditions.value)
+  const getPagination = computed(() => pagination.value)
+
+  // Actions
+  function setLoading(state) {
+    loading.value = state
+  }
+
+  function setError(errorMessage) {
+    error.value = errorMessage
+  }
+
+  function clearError() {
+    error.value = null
+  }
+
+  function setVendors(vendorList) {
+    vendors.value = vendorList
+  }
+
+  function setCurrentVendor(vendor) {
+    currentVendor.value = vendor
+  }
+
+  function updateSearchConditions(conditions) {
+    searchConditions.value = { ...searchConditions.value, ...conditions }
+  }
+
+  function updatePagination(paginationData) {
+    pagination.value = { ...pagination.value, ...paginationData }
+  }
+
+  function resetSearch() {
+    searchConditions.value = {
+      name: '',
+      category: '',
+      page: 1,
+      size: 10
+    }
+    pagination.value = {
+      currentPage: 1,
+      pageSize: 10,
+      totalCount: 0,
+      totalPages: 0
+    }
+  }
+
+  // API Actions
+  async function searchVendors(conditions = {}) {
+    setLoading(true)
+    clearError()
+    
+    try {
+      const searchParams = { ...searchConditions.value, ...conditions }
+      updateSearchConditions(searchParams)
+      
+      const response = await useAxios().get('/vendors', {
+        params: searchParams
+      })
+      
+      if (response.data) {
+        setVendors(response.data.vendors || [])
+        updatePagination({
+          currentPage: response.data.currentPage || 1,
+          totalCount: response.data.totalCount || 0,
+          totalPages: Math.ceil((response.data.totalCount || 0) / searchParams.size)
+        })
+      }
+    } catch (err) {
+      setError(err.message || '벤더사 검색 중 오류가 발생했습니다.')
+      setVendors([])
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  async function getVendorById(id) {
+    setLoading(true)
+    clearError()
+    
+    try {
+      const response = await useAxios().get(`/vendors/${id}`)
+      
+      if (response.data) {
+        setCurrentVendor(response.data)
+        return response.data
+      }
+    } catch (err) {
+      setError(err.message || '벤더사 정보를 불러오는 중 오류가 발생했습니다.')
+      setCurrentVendor(null)
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  return {
+    // State
+    vendors,
+    currentVendor,
+    loading,
+    error,
+    searchConditions,
+    pagination,
+    
+    // Getters
+    getVendors,
+    getCurrentVendor,
+    getLoading,
+    getError,
+    getSearchConditions,
+    getPagination,
+    
+    // Actions
+    setLoading,
+    setError,
+    clearError,
+    setVendors,
+    setCurrentVendor,
+    updateSearchConditions,
+    updatePagination,
+    resetSearch,
+    searchVendors,
+    getVendorById
+  }
+}, {
+  persist: {
+    storage: persistedState.sessionStorage,
+    paths: ['searchConditions', 'pagination']
+  }
+})

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini